diff --git a/.gitignore b/.gitignore index fb70e68f..ebd64b35 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,9 @@ next-env.d.ts # uploads /uploads +# download +/download + # cache /cache diff --git a/bun.lockb b/bun.lockb index b7d17ba0..e30c0976 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 791da768..e4b3bc0b 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.5", "private": true, "scripts": { - "dev": "bun --bun next dev --hostname 0.0.0.0", - "build": "bun --bun next build", - "start": "bun --bun next start" + "dev": "next dev", + "build": "next build", + "start": "next start" }, "prisma": { "seed": "bun run prisma/seed.ts" @@ -19,6 +19,7 @@ "@elysiajs/static": "^1.3.0", "@elysiajs/stream": "^1.1.0", "@elysiajs/swagger": "^1.2.0", + "@emotion/react": "^11.14.0", "@mantine/carousel": "^7.16.2", "@mantine/charts": "^7.17.1", "@mantine/core": "^7.17.4", @@ -26,6 +27,7 @@ "@mantine/dropzone": "^8.1.1", "@mantine/form": "^8.1.0", "@mantine/hooks": "^7.17.4", + "@mantine/modals": "^8.3.6", "@mantine/tiptap": "^7.17.4", "@paljs/types": "^8.1.0", "@prisma/client": "^6.3.1", @@ -39,19 +41,27 @@ "@tiptap/pm": "^2.11.7", "@tiptap/react": "^2.11.7", "@tiptap/starter-kit": "^2.11.7", + "@types/adm-zip": "^0.5.7", "@types/bun": "^1.2.2", "@types/leaflet": "^1.9.20", "@types/lodash": "^4.17.16", + "@types/nodemailer": "^7.0.2", "add": "^2.0.6", + "adm-zip": "^0.5.16", "animate.css": "^4.1.1", "bcryptjs": "^3.0.2", "bun": "^1.2.2", "chart.js": "^4.4.8", "classnames": "^2.5.1", + "colors": "^1.4.0", + "date-fns": "^4.1.0", "dayjs": "^1.11.13", + "dotenv": "^17.2.3", "elysia": "^1.3.5", - "embla-carousel-autoplay": "^8.5.2", - "embla-carousel-react": "^7.1.0", + "embla-carousel": "^8.6.0", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", + "extract-zip": "^2.0.1", "form-data": "^4.0.2", "framer-motion": "^12.23.5", "get-port": "^7.1.0", @@ -67,17 +77,20 @@ "next": "^15.5.2", "next-view-transitions": "^0.3.4", "node-fetch": "^3.3.2", + "nodemailer": "^7.0.10", "p-limit": "^6.2.0", "primeicons": "^7.0.0", "primereact": "^10.9.6", "prisma": "^6.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-exif-orientation-img": "^0.1.5", "react-international-phone": "^4.6.0", "react-leaflet": "^5.0.0", "react-simple-toasts": "^6.1.0", "react-toastify": "^11.0.5", "react-transition-group": "^4.4.5", + "react-zoom-pan-pinch": "^3.7.0", "readdirp": "^4.1.1", "recharts": "^2.15.3", "sharp": "^0.34.3", diff --git a/prisma/data/kategori-berita.json b/prisma/data/desa/berita/kategori-berita.json similarity index 87% rename from prisma/data/kategori-berita.json rename to prisma/data/desa/berita/kategori-berita.json index ee0a53b0..4d777965 100644 --- a/prisma/data/kategori-berita.json +++ b/prisma/data/desa/berita/kategori-berita.json @@ -1,5 +1,4 @@ [ - { "name": "Semua" }, { "name": "Pemerintahan" }, { "name": "Pembangunan" }, { "name": "Ekonomi" }, diff --git a/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json b/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json index 2259bea4..d39b6e98 100644 --- a/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json +++ b/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json @@ -1,6 +1,6 @@ [ { - "id": "1", + "id": "edit", "name": "Pelayanan Penduduk Non-Permanent", "deskripsi": "

Surat Keterangan Penduduk Non-Permanent adalah dokumen yang dikeluarkan oleh pihak berwenang untuk memberikan keterangan bahwa seseorang atau kelompok orang memiliki status penduduk non-permanent di suatu wilayah. Dokumen ini biasanya digunakan untuk keperluan administratif atau legal, seperti mendapatkan akses ke layanan kesehatan, pendidikan, atau pelayanan publik lainnya.

" } diff --git a/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json b/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json index 8df36cf0..42e7ab7a 100644 --- a/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json +++ b/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json @@ -1,6 +1,6 @@ [ { - "id": "1", + "id": "edit", "name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)", "deskripsi": "

Penyelenggaraan Perizinan Berusaha Berbasis Risiko melalui Sistem Online Single Submission (OSS) merupakan pelaksanaan Undang-Undang Nomor 11 Tahun 2020 Tentang Cipta Kerja. OSS Berbasis Risiko wajib digunakan oleh Pelaku Usaha, Kementerian/Lembaga, Pemerintah Daerah, Administrator Kawasan Ekonomi Khusus (KEK), dan Badan Pengusahaan Kawasan Perdagangan Bebas Pelabuhan Bebas (KPBPB).Berdasarkan Peraturan Pemerintah Nomor 5 Tahun 2021 terdapat 1.702 kegiatan usaha yang terdiri atas 1.349 Klasifikasi Baku Lapangan Usaha Indonesia (KBLI) yang sudah diimplementasikan dalam Sistem OSS Berbasis Risiko.

", "link" : "https://oss.go.id/" diff --git a/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json b/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json deleted file mode 100644 index 0f0e8271..00000000 --- a/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "id": "650e8400-e29b-41d4-a716-446655440001", - "atasanId": "550e8400-e29b-41d4-a716-446655440001", - "bawahanId": "550e8400-e29b-41d4-a716-446655440002", - "tipe": "Langsung Melapor" - } -] diff --git a/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json b/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json new file mode 100644 index 00000000..0976a812 --- /dev/null +++ b/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json @@ -0,0 +1,91 @@ +[ + { + "id": "cmgewz4gt000704ib91i3f169", + "namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.", + "gelarAkademik": "S.H.,M.H.,NL.P.", + "tanggalMasuk": "2020-01-01T00:00:00.000Z", + "email": "bagus@desa.id", + "telepon": "081234567891", + "alamat": "Jl. Raya Desa No. 1", + "posisiId": "kepala_desa", + "isActive": true + }, + { + "id": "cmgewxfvw000004ibee5013f4", + "namaLengkap": "I Ketut Suwanta", + "gelarAkademik": "S.Pt", + "tanggalMasuk": "2020-02-01T00:00:00.000Z", + "email": "suwanta@desa.id", + "telepon": "081234567892", + "alamat": "Jl. Raya Desa No. 2", + "posisiId": "sekretaris_desa", + "isActive": true + }, + { + "id": "cmgewxvqw000104ibgm5l8fzs", + "namaLengkap": "Ni Wayan Supardiati", + "gelarAkademik": "S.Pd", + "tanggalMasuk": "2020-02-01T00:00:00.000Z", + "email": "supardiati@desa.id", + "telepon": "081234567892", + "alamat": "Jl. Raya Desa No. 2", + "posisiId": "kaur_keuangan", + "isActive": true + }, + { + "id": "cmgewy1g9000204ib2n7hbx0i", + "namaLengkap": "I Wayan Agus Juni Artha Saputra", + "gelarAkademik": "S.T.", + "tanggalMasuk": "2020-02-01T00:00:00.000Z", + "email": "agus@desa.id", + "telepon": "081234567892", + "alamat": "Jl. Raya Desa No. 2", + "posisiId": "kadus_banjar_dinas_menesa", + "isActive": true + }, + { + "id": "cmgewybah000304ibgqhn1gm2", + "namaLengkap": "I Wayan Sueca", + "gelarAkademik": "S.H.", + "tanggalMasuk": "2020-02-01T00:00:00.000Z", + "email": "sueca@desa.id", + "telepon": "081234567893", + "alamat": "Jl. Raya Desa No. 2", + "posisiId": "kadus_banjar_dinas_darmasaba", + "isActive": true + }, + { + "id": "cmgewygqz000404ib20sv8nvg", + "namaLengkap": "Si Gede Ketut Astawa", + "gelarAkademik": "S.T.", + "tanggalMasuk": "2020-02-01T00:00:00.000Z", + "email": "astawa@desa.id", + "telepon": "081234567893", + "alamat": "Jl. Raya Desa No. 2", + "posisiId": "kadus_banjar_dinas_bucu", + "isActive": true + }, + { + "id": "cmgewyos1000504ibcu8o2gyk", + "namaLengkap": "I Kadek Arya Minarta", + "gelarAkademik": "S.T.", + "tanggalMasuk": "2020-02-01T00:00:00.000Z", + "email": "minarta@desa.id", + "telepon": "081234567893", + "alamat": "Jl. Raya Desa No. 2", + "posisiId": "kadus_banjar_dinas_gulingan", + "isActive": true + }, + { + "id": "cmgewyxk7000604ib8djs3i6c", + "namaLengkap": "I Gede Andika Pradnya Diputra", + "gelarAkademik": "S.E.", + "tanggalMasuk": "2020-02-01T00:00:00.000Z", + "email": "diputra@desa.id", + "telepon": "081234567893", + "alamat": "Jl. Raya Desa No. 2", + "posisiId": "kadus_banjar_dinas_taman", + "isActive": true + } + +] \ No newline at end of file diff --git a/prisma/data/ekonomi/struktur-organisasi/pegawai.json b/prisma/data/ekonomi/struktur-organisasi/pegawai.json deleted file mode 100644 index 9d7a6437..00000000 --- a/prisma/data/ekonomi/struktur-organisasi/pegawai.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "id": "550e8400-e29b-41d4-a716-446655440001", - "namaLengkap": "Budi Santoso", - "gelarAkademik": "S.IP", - "tanggalMasuk": "2020-01-01T00:00:00.000Z", - "email": "budi@desa.id", - "telepon": "081234567891", - "alamat": "Jl. Raya Desa No. 1", - "posisiId": "kepala_desa", - "isActive": true - }, - { - "id": "550e8400-e29b-41d4-a716-446655440002", - "namaLengkap": "Ani Lestari", - "gelarAkademik": "S.Pd", - "tanggalMasuk": "2020-02-01T00:00:00.000Z", - "email": "ani@desa.id", - "telepon": "081234567892", - "alamat": "Jl. Raya Desa No. 2", - "posisiId": "sekretaris_desa", - "isActive": true - } - ] \ No newline at end of file diff --git a/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json new file mode 100644 index 00000000..4a1699d7 --- /dev/null +++ b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json @@ -0,0 +1,159 @@ +[ + [ + { + "id": "kepala_desa", + "nama": "Kepala Desa", + "deskripsi": "Pemimpin desa Darmasaba", + "hierarki": 1, + "parentId": null + }, + { + "id": "kepala_urusan", + "nama": "Kepala Urusan", + "deskripsi": "Pemimpin urusan desa Darmasaba", + "hierarki": 2, + "parentId": "kepala_desa" + }, + { + "id": "sekretaris_desa", + "nama": "Sekretaris Desa", + "deskripsi": "Pengelola administrasi desa", + "hierarki": 2, + "parentId": "kepala_desa" + }, + { + "id": "kaur_keuangan", + "nama": "Kaur Keuangan", + "deskripsi": "Pengelola keuangan desa", + "hierarki": 3, + "parentId": "kaur_umum" + }, + { + "id": "kaur_perencanaan", + "nama": "Kaur Perencanaan", + "deskripsi": "Penyusun program kerja desa", + "hierarki": 3, + "parentId": "kaur_umum" + }, + { + "id": "kaur_umum", + "nama": "Kaur Umum & TU", + "deskripsi": "Pelayanan umum dan administrasi", + "hierarki": 2, + "parentId": "kepala_desa" + }, + { + "id": "kasi_pemerintahan", + "nama": "Kasi Pemerintahan", + "deskripsi": "Urusan pemerintahan dan keamanan", + "hierarki": 2, + "parentId": "kepala_desa" + }, + { + "id": "kasi_pelayanan", + "nama": "Kasi Pelayanan", + "deskripsi": "Urusan pelayanan masyarakat", + "hierarki": 2, + "parentId": "kepala_desa" + }, + { + "id": "kasi_kesejahteraan", + "nama": "Kasi Kesejahteraan", + "deskripsi": "Urusan sosial dan kesejahteraan", + "hierarki": 2, + "parentId": "kepala_desa" + }, + { + "id": "kadus_banjar_dinas_cabe", + "nama": "Kepala Dusun Banjar Dinas Cabe", + "deskripsi": "Pimpinan wilayah Banjar Dinas Cabe", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_menesa", + "nama": "Kepala Dusun Banjar Dinas Menesa", + "deskripsi": "Pimpinan wilayah Banjar Menesa", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_penenjoan", + "nama": "Kepala Dusun Banjar Dinas Penenjoan", + "deskripsi": "Pimpinan wilayah Banjar Dinas Penenjoan", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_telanga", + "nama": "Kepala Dusun Banjar Dinas Telanga", + "deskripsi": "Pimpinan wilayah Banjar Dinas Telanga", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_tengah", + "nama": "Kepala Dusun Banjar Dinas Tengah", + "deskripsi": "Pimpinan wilayah Banjar Dinas Tengah", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_baler_pasar", + "nama": "Kepala Dusun Banjar Dinas Baler Pasar", + "deskripsi": "Pimpinan wilayah Banjar Dinas Baler Pasar", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_bucu", + "nama": "Kepala Dusun Banjar Dinas Bucu", + "deskripsi": "Pimpinan wilayah Banjar Dinas Bucu", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_gulingan", + "nama": "Kepala Dusun Banjar Dinas Gulingan", + "deskripsi": "Pimpinan wilayah Banjar Dinas Gulingan", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_bersih", + "nama": "Kepala Dusun Banjar Dinas Bersih", + "deskripsi": "Pimpinan wilayah Banjar Dinas Bersih", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_umahanyar", + "nama": "Kepala Dusun Banjar Dinas Umahanyar", + "deskripsi": "Pimpinan wilayah Banjar Dinas Umahanyar", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_taman", + "nama": "Kepala Dusun Banjar Dinas Taman", + "deskripsi": "Pimpinan wilayah Banjar Dinas Taman", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "kadus_banjar_dinas_darmasaba", + "nama": "Kepala Dusun Banjar Dinas Darmasaba", + "deskripsi": "Pimpinan wilayah Banjar Dinas Darmasaba", + "hierarki": 3, + "parentId": "sekretaris_desa" + }, + { + "id": "staf_desa", + "nama": "Staf Desa", + "deskripsi": "Staf Desa", + "hierarki": 3, + "parentId": "sekretaris_desa" + } + ] + ] + \ No newline at end of file diff --git a/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json deleted file mode 100644 index 7596e168..00000000 --- a/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "id": "kepala_desa", - "nama": "Kepala Desa", - "deskripsi": "Kepala Desa", - "hierarki": 1 - }, - { - "id": "sekretaris_desa", - "nama": "Sekretaris Desa", - "deskripsi": "Sekretaris Desa", - "hierarki": 2 - }, - { - "id": "bendahara_desa", - "nama": "Bendahara Desa", - "deskripsi": "Bendahara Desa", - "hierarki": 3 - }, - { - "id": "staff_umum", - "nama": "Staff Umum", - "deskripsi": "Staff Umum", - "hierarki": 4 - } - ] - \ No newline at end of file diff --git a/prisma/data/landing-page/profile/mediaSosial.json b/prisma/data/landing-page/profile/mediaSosial.json index e6df4799..9af092a0 100644 --- a/prisma/data/landing-page/profile/mediaSosial.json +++ b/prisma/data/landing-page/profile/mediaSosial.json @@ -1,16 +1,4 @@ [ - { - "id": "cmds8w2q60002vnbe6i8qhkuo", - "name": "Telephone Desa Darmasaba", - "iconUrl": "081239580000", - "imageId": "cmff3nv180003vn6h5jvedidq" - }, - { - "id": "cmds8z7u20005vnbegyyvnbk0", - "name": "Email Desa Darmasaba", - "iconUrl": "desadarmasaba@badungkab.go.id", - "imageId": "cmff3ll130001vn6hkhls3f5y" - }, { "id": "cmds9023u0008vnbe3oxmhwyf", "name": "Desa Darmasaba", diff --git a/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json b/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json new file mode 100644 index 00000000..874e2e32 --- /dev/null +++ b/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json @@ -0,0 +1,6 @@ +[ + { "nama": "Kebersihan" }, + { "nama": "Infrastruktur" }, + { "nama": "Sosial" }, + { "nama": "Lingkungan" } + ] \ No newline at end of file diff --git a/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json b/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json new file mode 100644 index 00000000..a2d63947 --- /dev/null +++ b/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json @@ -0,0 +1,9 @@ +[ + { "id": "cmghqwjs4000404l8c5uvc300", "nama": "PAUD" }, + { "id": "cmghqwjs4000404l8c5uvc301", "nama": "TK" }, + { "id": "cmghqwjs4000404l8c5uvc302", "nama": "SD" }, + { "id": "cmghqwjs4000404l8c5uvc303", "nama": "SMP" }, + { "id": "cmghqwjs4000404l8c5uvc304", "nama": "SMA" }, + { "id": "cmghqwjs4000404l8c5uvc305", "nama": "SMK" } + ] + \ No newline at end of file diff --git a/prisma/data/ppid/struktur-ppid/pegawai-PPID.json b/prisma/data/ppid/struktur-ppid/pegawai-PPID.json index e7a7a9cf..713cb799 100644 --- a/prisma/data/ppid/struktur-ppid/pegawai-PPID.json +++ b/prisma/data/ppid/struktur-ppid/pegawai-PPID.json @@ -1,6 +1,6 @@ [ { - "id": "550e8400-e29b-41d4-a716-446655440001", + "id": "cmgewz4gt000704ib91i3f169", "namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.", "gelarAkademik": "S.H.,M.H.,NL.P.", "tanggalMasuk": "2020-01-01T00:00:00.000Z", @@ -11,7 +11,7 @@ "isActive": true }, { - "id": "550e8400-e29b-41d4-a716-446655440002", + "id": "cmgewxfvw000004ibee5013f4", "namaLengkap": "I Ketut Suwanta", "gelarAkademik": "S.Pt", "tanggalMasuk": "2020-02-01T00:00:00.000Z", @@ -22,7 +22,7 @@ "isActive": true }, { - "id": "550e8400-e29b-41d4-a716-446655440006", + "id": "cmgewxvqw000104ibgm5l8fzs", "namaLengkap": "Ni Wayan Supardiati", "gelarAkademik": "S.Pd", "tanggalMasuk": "2020-02-01T00:00:00.000Z", @@ -33,7 +33,7 @@ "isActive": true }, { - "id": "550e8400-e29b-41d4-a716-446655440011", + "id": "cmgewy1g9000204ib2n7hbx0i", "namaLengkap": "I Wayan Agus Juni Artha Saputra", "gelarAkademik": "S.T.", "tanggalMasuk": "2020-02-01T00:00:00.000Z", @@ -44,7 +44,7 @@ "isActive": true }, { - "id": "550e8400-e29b-41d4-a716-446655440012", + "id": "cmgewybah000304ibgqhn1gm2", "namaLengkap": "I Wayan Sueca", "gelarAkademik": "S.H.", "tanggalMasuk": "2020-02-01T00:00:00.000Z", @@ -55,7 +55,7 @@ "isActive": true }, { - "id": "550e8400-e29b-41d4-a716-446655440017", + "id": "cmgewygqz000404ib20sv8nvg", "namaLengkap": "Si Gede Ketut Astawa", "gelarAkademik": "S.T.", "tanggalMasuk": "2020-02-01T00:00:00.000Z", @@ -66,7 +66,7 @@ "isActive": true }, { - "id": "550e8400-e29b-41d4-a716-446655440018", + "id": "cmgewyos1000504ibcu8o2gyk", "namaLengkap": "I Kadek Arya Minarta", "gelarAkademik": "S.T.", "tanggalMasuk": "2020-02-01T00:00:00.000Z", @@ -77,7 +77,7 @@ "isActive": true }, { - "id": "550e8400-e29b-41d4-a716-446655440021", + "id": "cmgewyxk7000604ib8djs3i6c", "namaLengkap": "I Gede Andika Pradnya Diputra", "gelarAkademik": "S.E.", "tanggalMasuk": "2020-02-01T00:00:00.000Z", diff --git a/prisma/data/user/roles.json b/prisma/data/user/roles.json index 79da3188..4d2a1046 100644 --- a/prisma/data/user/roles.json +++ b/prisma/data/user/roles.json @@ -1,29 +1,32 @@ [ - { - "id": "1", - "name": "ADMIN DESA", - "description": "Administrator Desa", - "permissions": ["manage_users", "manage_content", "view_reports"], - "isActive": true, - "createdAt": "2025-09-01T00:00:00.000Z", - "updatedAt": "2025-09-01T00:00:00.000Z" - }, - { - "id": "2", - "name": "ADMIN KESEHATAN", - "description": "Administrator Bidang Kesehatan", - "permissions": ["manage_health_data", "view_reports"], - "isActive": true, - "createdAt": "2025-09-01T00:00:00.000Z", - "updatedAt": "2025-09-01T00:00:00.000Z" - }, - { - "id": "3", - "name": "ADMIN SEKOLAH", - "description": "Administrator Sekolah", - "permissions": ["manage_school_data", "view_reports"], - "isActive": true, - "createdAt": "2025-09-01T00:00:00.000Z", - "updatedAt": "2025-09-01T00:00:00.000Z" - } - ] \ No newline at end of file + { + "id": "0", + "name": "DEVELOPER", + "description": "Developer", + "isActive": true + }, + { + "id": "1", + "name": "SUPER ADMIN", + "description": "Administrator", + "isActive": true + }, + { + "id": "2", + "name": "ADMIN DESA", + "description": "Administrator Desa", + "isActive": true + }, + { + "id": "3", + "name": "ADMIN KESEHATAN", + "description": "Administrator Bidang Kesehatan", + "isActive": true + }, + { + "id": "4", + "name": "ADMIN PENDIDIKAN", + "description": "Administrator Bidang Pendidikan", + "isActive": true + } +] diff --git a/prisma/data/user/users.json b/prisma/data/user/users.json index 2f44c667..733aeba4 100644 --- a/prisma/data/user/users.json +++ b/prisma/data/user/users.json @@ -1,32 +1,10 @@ [ - { - "id": "1", - "nama": "Admin Desa", - "nomor": "089647037426", - "roleId": "1", - "isActive": true, - "lastLogin": "2025-08-31T10:00:00.000Z", - "createdAt": "2025-09-01T00:00:00.000Z", - "updatedAt": "2025-09-01T00:00:00.000Z" - }, - { - "id": "2", - "nama": "Admin Kesehatan", - "nomor": "082339004198", - "roleId": "2", - "isActive": true, - "lastLogin": null, - "createdAt": "2025-09-01T00:00:00.000Z", - "updatedAt": "2025-09-01T00:00:00.000Z" - }, - { - "id": "3", - "nama": "Admin Sekolah", - "nomor": "085237157222", - "roleId": "3", - "isActive": true, - "lastLogin": null, - "createdAt": "2025-09-01T00:00:00.000Z", - "updatedAt": "2025-09-01T00:00:00.000Z" - } - ] \ No newline at end of file + { + "id": "cmie1o0zh0002vn132vtzg7hh", + "username": "SuperAdmin-Nico", + "nomor": "6289647037426", + "roleId": 0, + "isActive": true, + "sessionInvalid": false + } +] diff --git a/prisma/migrations/20251119062255_add_unique_username/migration.sql b/prisma/migrations/20251119062255_add_unique_username/migration.sql new file mode 100644 index 00000000..d8d9e087 --- /dev/null +++ b/prisma/migrations/20251119062255_add_unique_username/migration.sql @@ -0,0 +1,1127 @@ +/* + Warnings: + + - You are about to drop the column `kelahiranKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `kematianBayi` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `kematianKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `tahun` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `jumlah` on the `GrafikKepuasan` table. All the data in the column will be lost. + - You are about to drop the column `label` on the `GrafikKepuasan` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KolaborasiInovasi` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KontakDaruratKeamanan` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KontakItem` table. All the data in the column will be lost. + - You are about to drop the column `kategoriId` on the `KontakItem` table. All the data in the column will be lost. + - You are about to drop the column `programKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `tipsKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `videoKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `kategori` on the `PotensiDesa` table. All the data in the column will be lost. + - You are about to drop the column `ikonUrl` on the `ProgramKemiskinan` table. All the data in the column will be lost. + - You are about to drop the `ProgramKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `TipsKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `VideoKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `hubungan_organisasi` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `pegawai` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `posisi_organisasi` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `struktur_organisasi` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `kelahiranId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty. + - Added the required column `kematianId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty. + - Added the required column `category` to the `FileStorage` table without a default value. This is not possible if the table is not empty. + - Added the required column `alamat` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `jenisKelamin` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `nama` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `penyakit` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `tanggal` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `whatsapp` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty. + - Added the required column `kategoriId` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `KontakItem` table without a default value. This is not possible if the table is not empty. + - Added the required column `notelp` to the `LowonganPekerjaan` table without a default value. This is not possible if the table is not empty. + - Added the required column `name` to the `MediaSosial` table without a default value. This is not possible if the table is not empty. + - Added the required column `kontak` to the `PasarDesa` table without a default value. This is not possible if the table is not empty. + - Added the required column `deskripsi` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `deskripsiSingkat` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `judul` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `linkVideo` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `jadwalPelayanan` to the `Posyandu` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `ProgramKemiskinan` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "JenisKelamin" AS ENUM ('LAKI_LAKI', 'PEREMPUAN'); + +-- CreateEnum +CREATE TYPE "Agama" AS ENUM ('ISLAM', 'KRISTEN_PROTESTAN', 'KRISTEN_KATOLIK', 'HINDU', 'BUDDHA', 'KONGHUCU', 'LAINNYA'); + +-- CreateEnum +CREATE TYPE "StatusPernikahan" AS ENUM ('BELUM_MENIKAH', 'MENIKAH', 'JANDA_DUDA'); + +-- CreateEnum +CREATE TYPE "UkuranBaju" AS ENUM ('S', 'M', 'L', 'XL', 'XXL', 'LAINNYA'); + +-- CreateEnum +CREATE TYPE "StatusPeminjaman" AS ENUM ('Dipinjam', 'Dikembalikan', 'Terlambat', 'Dibatalkan'); + +-- DropForeignKey +ALTER TABLE "JadwalKegiatan" DROP CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey"; + +-- DropForeignKey +ALTER TABLE "KolaborasiInovasi" DROP CONSTRAINT "KolaborasiInovasi_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakDaruratKeamanan" DROP CONSTRAINT "KontakDaruratKeamanan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_kategoriId_fkey"; + +-- DropForeignKey +ALTER TABLE "MediaSosial" DROP CONSTRAINT "MediaSosial_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PelayananSuratKeterangan" DROP CONSTRAINT "PelayananSuratKeterangan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_programKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_tipsKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_videoKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "Penghargaan" DROP CONSTRAINT "Penghargaan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PotensiDesa" DROP CONSTRAINT "PotensiDesa_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_atasanId_fkey"; + +-- DropForeignKey +ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_bawahanId_fkey"; + +-- DropForeignKey +ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_posisiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_hubunganOrganisasiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_pegawaiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_posisiOrganisasiId_fkey"; + +-- AlterTable +ALTER TABLE "ArtikelKesehatan" ADD COLUMN "imageId" TEXT; + +-- AlterTable +ALTER TABLE "DataKematian_Kelahiran" DROP COLUMN "kelahiranKasar", +DROP COLUMN "kematianBayi", +DROP COLUMN "kematianKasar", +DROP COLUMN "tahun", +ADD COLUMN "kelahiranId" TEXT NOT NULL, +ADD COLUMN "kematianId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "FileStorage" ADD COLUMN "category" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "GrafikKepuasan" DROP COLUMN "jumlah", +DROP COLUMN "label", +ADD COLUMN "alamat" TEXT NOT NULL, +ADD COLUMN "jenisKelamin" TEXT NOT NULL, +ADD COLUMN "nama" TEXT NOT NULL, +ADD COLUMN "penyakit" TEXT NOT NULL, +ADD COLUMN "tanggal" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "JadwalKegiatan" ALTER COLUMN "pendaftaranJadwalKegiatanId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "KolaborasiInovasi" DROP COLUMN "imageId"; + +-- AlterTable +ALTER TABLE "KontakDarurat" ADD COLUMN "whatsapp" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "imageId", +ADD COLUMN "deletedAt" TIMESTAMP(3), +ADD COLUMN "icon" TEXT NOT NULL, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "kategoriId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "KontakItem" DROP COLUMN "imageId", +DROP COLUMN "kategoriId", +ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "icon" TEXT NOT NULL, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true; + +-- AlterTable +ALTER TABLE "LaporanPublik" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, +ALTER COLUMN "status" SET DEFAULT 'Proses'; + +-- AlterTable +ALTER TABLE "LowonganPekerjaan" ADD COLUMN "notelp" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "MediaSosial" ADD COLUMN "name" TEXT NOT NULL, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "PasarDesa" ADD COLUMN "kontak" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "PelayananSuratKeterangan" ADD COLUMN "image2Id" TEXT, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "PencegahanKriminalitas" DROP COLUMN "programKeamananId", +DROP COLUMN "tipsKeamananId", +DROP COLUMN "videoKeamananId", +ADD COLUMN "deskripsi" TEXT NOT NULL, +ADD COLUMN "deskripsiSingkat" TEXT NOT NULL, +ADD COLUMN "judul" TEXT NOT NULL, +ADD COLUMN "linkVideo" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Penghargaan" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Posyandu" ADD COLUMN "jadwalPelayanan" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "PotensiDesa" DROP COLUMN "kategori", +ADD COLUMN "kategoriId" TEXT, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "ProgramKemiskinan" DROP COLUMN "ikonUrl", +ADD COLUMN "icon" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "StrukturPPID" ADD COLUMN "pegawaiPPIDId" TEXT, +ADD COLUMN "posisiOrganisasiPPIDId" TEXT; + +-- DropTable +DROP TABLE "ProgramKeamanan"; + +-- DropTable +DROP TABLE "TipsKeamanan"; + +-- DropTable +DROP TABLE "VideoKeamanan"; + +-- DropTable +DROP TABLE "hubungan_organisasi"; + +-- DropTable +DROP TABLE "pegawai"; + +-- DropTable +DROP TABLE "posisi_organisasi"; + +-- DropTable +DROP TABLE "struktur_organisasi"; + +-- CreateTable +CREATE TABLE "DesaAntiKorupsi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "fileId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DesaAntiKorupsi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriDesaAntiKorupsi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriDesaAntiKorupsi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SdgsDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "jumlah" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "SdgsDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APBDes" ( + "id" TEXT NOT NULL, + "tahun" INTEGER, + "name" TEXT, + "deskripsi" TEXT, + "jumlah" TEXT, + "imageId" TEXT, + "fileId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "APBDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APBDesItem" ( + "id" TEXT NOT NULL, + "kode" TEXT NOT NULL, + "uraian" TEXT NOT NULL, + "anggaran" DOUBLE PRECISION NOT NULL, + "realisasi" DOUBLE PRECISION NOT NULL, + "selisih" DOUBLE PRECISION NOT NULL, + "persentase" DOUBLE PRECISION NOT NULL, + "tipe" TEXT, + "level" INTEGER NOT NULL, + "parentId" TEXT, + "apbdesId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "APBDesItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PrestasiDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PrestasiDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriPrestasiDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriPrestasiDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Responden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "tanggal" DATE NOT NULL, + "jenisKelaminId" TEXT NOT NULL, + "ratingId" TEXT NOT NULL, + "kelompokUmurId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Responden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenisKelaminResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenisKelaminResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PilihanRatingResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PilihanRatingResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UmurResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "UmurResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PosisiOrganisasiPPID" ( + "id" TEXT NOT NULL, + "nama" VARCHAR(100) NOT NULL, + "deskripsi" TEXT, + "hierarki" INTEGER NOT NULL, + "parentId" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PosisiOrganisasiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PegawaiPPID" ( + "id" TEXT NOT NULL, + "namaLengkap" VARCHAR(255) NOT NULL, + "gelarAkademik" VARCHAR(100), + "imageId" TEXT, + "tanggalMasuk" DATE, + "email" VARCHAR(255), + "telepon" VARCHAR(20), + "alamat" TEXT, + "posisiId" VARCHAR(50) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PegawaiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturOrganisasiPPID" ( + "id" TEXT NOT NULL, + "posisiOrganisasiId" VARCHAR(50) NOT NULL, + "pegawaiId" TEXT NOT NULL, + "hubunganOrganisasiId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "StrukturOrganisasiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PerbekelDariMasaKeMasa" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "periode" TEXT NOT NULL, + "imageId" TEXT, + "daerah" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PerbekelDariMasaKeMasa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriPotensi" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriPotensi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AjukanPermohonan" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "nik" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "nomorKk" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "AjukanPermohonan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Kelahiran" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "tanggal" TIMESTAMP(3) NOT NULL, + "jenisKelamin" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Kelahiran_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Kematian" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "tanggal" TIMESTAMP(3) NOT NULL, + "jenisKelamin" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "penyebab" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Kematian_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KontakDaruratToItem" ( + "id" TEXT NOT NULL, + "kontakDaruratId" TEXT NOT NULL, + "kontakItemId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "KontakDaruratToItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturBumDes" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "posisiOrganisasiBumDesId" TEXT, + "pegawaiBumDesId" TEXT, + + CONSTRAINT "StrukturBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PosisiOrganisasiBumDes" ( + "id" TEXT NOT NULL, + "nama" VARCHAR(100) NOT NULL, + "deskripsi" TEXT, + "hierarki" INTEGER NOT NULL, + "parentId" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PosisiOrganisasiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PegawaiBumDes" ( + "id" TEXT NOT NULL, + "namaLengkap" VARCHAR(255) NOT NULL, + "gelarAkademik" VARCHAR(100), + "imageId" TEXT, + "tanggalMasuk" DATE, + "email" VARCHAR(255), + "telepon" VARCHAR(20), + "alamat" TEXT, + "posisiId" VARCHAR(50) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PegawaiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturOrganisasiBumDes" ( + "id" TEXT NOT NULL, + "posisiOrganisasiId" VARCHAR(50) NOT NULL, + "pegawaiId" TEXT NOT NULL, + "hubunganOrganisasiId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "StrukturOrganisasiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MitraKolaborasi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "MitraKolaborasi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenjangPendidikan" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenjangPendidikan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Lembaga" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "jenjangId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Lembaga_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Siswa" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "lembagaId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Siswa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Pengajar" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "lembagaId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Pengajar_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KeunggulanProgram" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KeunggulanProgram_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BeasiswaPendaftar" ( + "id" TEXT NOT NULL, + "namaLengkap" TEXT NOT NULL, + "nis" TEXT, + "kelas" TEXT, + "jenisKelamin" "JenisKelamin" NOT NULL, + "alamatDomisili" TEXT, + "tempatLahir" TEXT NOT NULL, + "tanggalLahir" TIMESTAMP(3) NOT NULL, + "namaOrtu" TEXT, + "nik" TEXT NOT NULL, + "pekerjaanOrtu" TEXT, + "penghasilan" TEXT, + "noHp" TEXT NOT NULL, + "kewarganegaraan" TEXT, + "agama" "Agama", + "alamatKTP" TEXT, + "email" TEXT, + "statusPernikahan" "StatusPernikahan", + "ukuranBaju" "UkuranBaju", + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BeasiswaPendaftar_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanProgram" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanProgram_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProgramUnggulan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "ProgramUnggulan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LokasiJadwalBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "LokasiJadwalBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FasilitasBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "FasilitasBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanPendidikanNonFormal" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanPendidikanNonFormal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TempatKegiatan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TempatKegiatan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenisProgramYangDiselenggarakan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenisProgramYangDiselenggarakan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DataPerpustakaan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "imageId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DataPerpustakaan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriBuku" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriBuku_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PeminjamanBuku" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "noTelp" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "bukuId" TEXT NOT NULL, + "tanggalPinjam" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "batasKembali" TIMESTAMP(3) NOT NULL, + "tanggalKembali" TIMESTAMP(3), + "status" "StatusPeminjaman" NOT NULL DEFAULT 'Dipinjam', + "catatan" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PeminjamanBuku_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "nomor" TEXT NOT NULL, + "roleId" TEXT NOT NULL DEFAULT '1', + "instansi" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "lastLogin" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "roles" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "permissions" JSONB NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "roles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KodeOtp" ( + "id" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "nomor" TEXT NOT NULL, + "otp" INTEGER NOT NULL, + + CONSTRAINT "KodeOtp_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "permissions" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "permissions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserSession" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3), + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + + CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DataPendidikan" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "jumlah" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DataPendidikan_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DesaAntiKorupsi_name_key" ON "DesaAntiKorupsi"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "KategoriDesaAntiKorupsi_name_key" ON "KategoriDesaAntiKorupsi"("name"); + +-- CreateIndex +CREATE INDEX "APBDesItem_kode_idx" ON "APBDesItem"("kode"); + +-- CreateIndex +CREATE INDEX "APBDesItem_level_idx" ON "APBDesItem"("level"); + +-- CreateIndex +CREATE INDEX "APBDesItem_apbdesId_idx" ON "APBDesItem"("apbdesId"); + +-- CreateIndex +CREATE UNIQUE INDEX "KategoriPrestasiDesa_name_key" ON "KategoriPrestasiDesa"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Responden_name_key" ON "Responden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "JenisKelaminResponden_name_key" ON "JenisKelaminResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PilihanRatingResponden_name_key" ON "PilihanRatingResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "UmurResponden_name_key" ON "UmurResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PegawaiPPID_email_key" ON "PegawaiPPID"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "PegawaiBumDes_email_key" ON "PegawaiBumDes"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "BeasiswaPendaftar_nik_key" ON "BeasiswaPendaftar"("nik"); + +-- CreateIndex +CREATE UNIQUE INDEX "BeasiswaPendaftar_email_key" ON "BeasiswaPendaftar"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_nomor_key" ON "User"("nomor"); + +-- CreateIndex +CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "permissions_name_key" ON "permissions"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSession_userId_key" ON "UserSession"("userId"); + +-- AddForeignKey +ALTER TABLE "MediaSosial" ADD CONSTRAINT "MediaSosial_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriDesaAntiKorupsi"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SdgsDesa" ADD CONSTRAINT "SdgsDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "APBDesItem"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_apbdesId_fkey" FOREIGN KEY ("apbdesId") REFERENCES "APBDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPrestasiDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_jenisKelaminId_fkey" FOREIGN KEY ("jenisKelaminId") REFERENCES "JenisKelaminResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_ratingId_fkey" FOREIGN KEY ("ratingId") REFERENCES "PilihanRatingResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_kelompokUmurId_fkey" FOREIGN KEY ("kelompokUmurId") REFERENCES "UmurResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_posisiOrganisasiPPIDId_fkey" FOREIGN KEY ("posisiOrganisasiPPIDId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_pegawaiPPIDId_fkey" FOREIGN KEY ("pegawaiPPIDId") REFERENCES "PegawaiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PosisiOrganisasiPPID" ADD CONSTRAINT "PosisiOrganisasiPPID_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PerbekelDariMasaKeMasa" ADD CONSTRAINT "PerbekelDariMasaKeMasa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPotensi"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_image2Id_fkey" FOREIGN KEY ("image2Id") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AjukanPermohonan" ADD CONSTRAINT "AjukanPermohonan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "PelayananSuratKeterangan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Penghargaan" ADD CONSTRAINT "Penghargaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey" FOREIGN KEY ("pendaftaranJadwalKegiatanId") REFERENCES "PendaftaranJadwalKegiatan"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kematianId_fkey" FOREIGN KEY ("kematianId") REFERENCES "Kematian"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kelahiranId_fkey" FOREIGN KEY ("kelahiranId") REFERENCES "Kelahiran"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratKeamanan" ADD CONSTRAINT "KontakDaruratKeamanan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakDaruratId_fkey" FOREIGN KEY ("kontakDaruratId") REFERENCES "KontakDaruratKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakItemId_fkey" FOREIGN KEY ("kontakItemId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_posisiOrganisasiBumDesId_fkey" FOREIGN KEY ("posisiOrganisasiBumDesId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_pegawaiBumDesId_fkey" FOREIGN KEY ("pegawaiBumDesId") REFERENCES "PegawaiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PosisiOrganisasiBumDes" ADD CONSTRAINT "PosisiOrganisasiBumDes_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MitraKolaborasi" ADD CONSTRAINT "MitraKolaborasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Lembaga" ADD CONSTRAINT "Lembaga_jenjangId_fkey" FOREIGN KEY ("jenjangId") REFERENCES "JenjangPendidikan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Siswa" ADD CONSTRAINT "Siswa_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Pengajar" ADD CONSTRAINT "Pengajar_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriBuku"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PeminjamanBuku" ADD CONSTRAINT "PeminjamanBuku_bukuId_fkey" FOREIGN KEY ("bukuId") REFERENCES "DataPerpustakaan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/safeseedUnique.ts b/prisma/safeseedUnique.ts new file mode 100644 index 00000000..92d16071 --- /dev/null +++ b/prisma/safeseedUnique.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// helpers/safeSeedUnique.ts +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +/** + * Helper generic buat seed dengan upsert aman + */ +export async function safeSeedUnique( + model: T, + where: Record, + data: Record +) { + const m = prisma[model]; + + if (!m) throw new Error(`Model ${String(model)} tidak ditemukan di PrismaClient`); + + try { + // @ts-expect-error upsert dynamic + await m.upsert({ + where, + update: data, + create: { ...where, ...data }, + }); + console.log(`✅ Seeded ${String(model)} -> ${JSON.stringify(where)}`); + } catch (err) { + console.error(`❌ Gagal seed ${String(model)} -> ${JSON.stringify(where)}`, err); + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 54166bfc..1e73563b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -81,7 +81,7 @@ model FileStorage { PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage") PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2") PasarDesa PasarDesa[] - Pegawai Pegawai[] + PegawaiBumDes PegawaiBumDes[] DesaDigital DesaDigital[] InfoTekno InfoTekno[] PengaduanMasyarakat PengaduanMasyarakat[] @@ -101,6 +101,7 @@ model FileStorage { MitraKolaborasi MitraKolaborasi[] ArtikelKesehatan ArtikelKesehatan[] + StrukturBumDes StrukturBumDes[] } //========================================= MENU LANDING PAGE ========================================= // @@ -142,7 +143,7 @@ model MediaSosial { isActive Boolean @default(true) } -//========================================= PROFILE ========================================= // +//========================================= DESA ANTI KORUPSI ========================================= // model DesaAntiKorupsi { id String @id @default(cuid()) name String @unique @@ -183,18 +184,46 @@ model SdgsDesa { //========================================= APBDes ========================================= // model APBDes { id String @id @default(cuid()) - name String - jumlah String + tahun Int? + name String? // misalnya: "APBDes Tahun 2025" + deskripsi String? + jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items) + items APBDesItem[] image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id]) imageId String? file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id]) fileId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? // opsional, tidak perlu default now() isActive Boolean @default(true) } +model APBDesItem { + id String @id @default(cuid()) + kode String // contoh: "4", "4.1", "4.1.2" + uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha" + anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS) + realisasi Float + selisih Float // realisasi - anggaran + persentase Float + tipe String? // (realisasi / anggaran) * 100 + level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail + parentId String? // untuk relasi hierarki + parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id]) + children APBDesItem[] @relation("APBDesItemParent") + apbdesId String + apbdes APBDes @relation(fields: [apbdesId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + isActive Boolean @default(true) + + @@index([kode]) + @@index([level]) + @@index([apbdesId]) +} + //========================================= PRESTASI DESA ========================================= // model PrestasiDesa { id String @id @default(cuid()) @@ -286,49 +315,51 @@ model StrukturPPID { } model PosisiOrganisasiPPID { - id String @id @default(cuid()) - nama String @db.VarChar(100) - deskripsi String? @db.Text - hierarki Int - pegawai PegawaiPPID[] - strukturOrganisasi StrukturPPID[] // Relasi balik - parentId String? - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id]) - children PosisiOrganisasiPPID[] @relation("Parent") + id String @id @default(cuid()) + nama String @db.VarChar(100) + deskripsi String? @db.Text + hierarki Int + pegawai PegawaiPPID[] + strukturOrganisasi StrukturPPID[] // Relasi balik + parentId String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id]) + children PosisiOrganisasiPPID[] @relation("Parent") + StrukturOrganisasiPPID StrukturOrganisasiPPID[] } model PegawaiPPID { - id String @id @default(cuid()) - namaLengkap String @db.VarChar(255) - gelarAkademik String? @db.VarChar(100) - image FileStorage? @relation(fields: [imageId], references: [id]) - imageId String? - tanggalMasuk DateTime? @db.Date - email String? @unique @db.VarChar(255) - telepon String? @db.VarChar(20) - alamat String? @db.Text - posisiId String @db.VarChar(50) - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id]) - strukturOrganisasi StrukturPPID[] // Relasi balik + id String @id @default(cuid()) + namaLengkap String @db.VarChar(255) + gelarAkademik String? @db.VarChar(100) + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + tanggalMasuk DateTime? @db.Date + email String? @unique @db.VarChar(255) + telepon String? @db.VarChar(20) + alamat String? @db.Text + posisiId String @db.VarChar(50) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id]) + strukturOrganisasi StrukturPPID[] // Relasi balik + StrukturOrganisasiPPID StrukturOrganisasiPPID[] } model StrukturOrganisasiPPID { - id String @id @default(uuid()) - posisiOrganisasiId String @db.VarChar(50) - pegawaiId String @db.Uuid - hubunganOrganisasiId String @db.Uuid - posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id]) - pegawai Pegawai @relation(fields: [pegawaiId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + posisiOrganisasiId String @db.VarChar(50) + pegawaiId String + hubunganOrganisasiId String + posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id]) + pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt deletedAt DateTime? - isActive Boolean @default(true) + isActive Boolean @default(true) } // ========================================= VISI MISI PPID ========================================= // @@ -850,7 +881,7 @@ model JadwalKegiatan { syaratKetentuanJadwalKegiatanId String dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id]) dokumenJadwalKegiatanId String - pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id]) + pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id]) pendaftaranJadwalKegiatanId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -1167,6 +1198,7 @@ model KontakDarurat { deskripsi String image FileStorage @relation(fields: [imageId], references: [id]) imageId String + whatsapp String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -1340,6 +1372,7 @@ model PasarDesa { harga Int rating Float alamatUsaha String + kontak String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -1382,6 +1415,7 @@ model LowonganPekerjaan { gaji String deskripsi String kualifikasi String + notelp String tanggalPosting DateTime @default(now()) isActive Boolean @default(true) createdAt DateTime @default(now()) @@ -1391,79 +1425,67 @@ model LowonganPekerjaan { // ========================================= STRUKTUR ORGANISASI ========================================= // -model PosisiOrganisasi { - id String @id @default(uuid()) @db.VarChar(50) - nama String @db.VarChar(100) - deskripsi String? @db.Text - hierarki Int - - pegawai Pegawai[] - strukturOrganisasi StrukturOrganisasi[] // Relasi balik - StrukturOrganisasiPPID StrukturOrganisasiPPID[] - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@map("posisi_organisasi") +model StrukturBumDes { + id String @id @default(cuid()) + name String @db.Text + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + PosisiOrganisasiBumDes PosisiOrganisasiBumDes? @relation(fields: [posisiOrganisasiBumDesId], references: [id]) + posisiOrganisasiBumDesId String? + PegawaiBumDes PegawaiBumDes? @relation(fields: [pegawaiBumDesId], references: [id]) + pegawaiBumDesId String? } -model Pegawai { - id String @id @default(uuid()) @db.Uuid - namaLengkap String @db.VarChar(255) - gelarAkademik String? @db.VarChar(100) - image FileStorage? @relation(fields: [imageId], references: [id]) - imageId String? - tanggalMasuk DateTime? @db.Date - email String? @unique @db.VarChar(255) - telepon String? @db.VarChar(20) - alamat String? @db.Text - posisiId String @db.VarChar(50) - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - posisi PosisiOrganisasi @relation(fields: [posisiId], references: [id]) - - sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan") - sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan") - - strukturOrganisasi StrukturOrganisasi[] // Relasi balik - StrukturOrganisasiPPID StrukturOrganisasiPPID[] - - @@map("pegawai") +model PosisiOrganisasiBumDes { + id String @id @default(cuid()) + nama String @db.VarChar(100) + deskripsi String? @db.Text + hierarki Int + pegawai PegawaiBumDes[] + strukturOrganisasi StrukturBumDes[] // Relasi balik + parentId String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + parent PosisiOrganisasiBumDes? @relation("Parent", fields: [parentId], references: [id]) + children PosisiOrganisasiBumDes[] @relation("Parent") + StrukturOrganisasiBumDes StrukturOrganisasiBumDes[] } -model HubunganOrganisasi { - id String @id @default(uuid()) @db.Uuid - atasanId String @db.Uuid - bawahanId String @db.Uuid - tipe String? @db.VarChar(50) - - atasan Pegawai @relation("AtasanToBawahan", fields: [atasanId], references: [id]) - bawahan Pegawai @relation("BawahanToAtasan", fields: [bawahanId], references: [id]) - - strukturOrganisasi StrukturOrganisasi[] // Relasi balik - - @@unique([atasanId, bawahanId]) - @@map("hubungan_organisasi") +model PegawaiBumDes { + id String @id @default(cuid()) + namaLengkap String @db.VarChar(255) + gelarAkademik String? @db.VarChar(100) + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + tanggalMasuk DateTime? @db.Date + email String? @unique @db.VarChar(255) + telepon String? @db.VarChar(20) + alamat String? @db.Text + posisiId String @db.VarChar(50) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + posisi PosisiOrganisasiBumDes @relation(fields: [posisiId], references: [id]) + strukturOrganisasi StrukturBumDes[] // Relasi balik + StrukturOrganisasiBumDes StrukturOrganisasiBumDes[] } -model StrukturOrganisasi { - id String @id @default(uuid()) - posisiOrganisasiId String @db.VarChar(50) - pegawaiId String @db.Uuid - hubunganOrganisasiId String @db.Uuid - - posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id]) - pegawai Pegawai @relation(fields: [pegawaiId], references: [id]) - hubunganOrganisasi HubunganOrganisasi @relation(fields: [hubunganOrganisasiId], references: [id]) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - isActive Boolean @default(true) - - @@map("struktur_organisasi") +model StrukturOrganisasiBumDes { + id String @id @default(uuid()) + posisiOrganisasiId String @db.VarChar(50) + pegawaiId String + hubunganOrganisasiId String + posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id]) + pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + isActive Boolean @default(true) } // ========================================= PROGRAM KEMISKINAN ========================================= // @@ -1612,7 +1634,7 @@ model Pembiayaan { ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan") } -// ========================================= INOVASI ========================================= // +// ========================================= MENU INOVASI ========================================= // // ========================================= DESA DIGITAL / SMART VILLAGE ========================================= // model DesaDigital { id String @id @default(cuid()) @@ -1948,23 +1970,28 @@ model KeunggulanProgram { } model BeasiswaPendaftar { - id String @id @default(cuid()) + id String @id @default(cuid()) namaLengkap String - nik String @unique + nis String? + kelas String? + jenisKelamin JenisKelamin + alamatDomisili String? tempatLahir String tanggalLahir DateTime - jenisKelamin JenisKelamin - kewarganegaraan String - agama Agama - alamatKTP String - alamatDomisili String? + namaOrtu String? + nik String @unique + pekerjaanOrtu String? + penghasilan String? noHp String - email String @unique - statusPernikahan StatusPernikahan + kewarganegaraan String? + agama Agama? + alamatKTP String? + email String? @unique + statusPernikahan StatusPernikahan? ukuranBaju UkuranBaju? - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } enum JenisKelamin { @@ -2093,6 +2120,9 @@ model DataPerpustakaan { updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) isActive Boolean @default(true) + + // relasi baru ke peminjaman + peminjamanBuku PeminjamanBuku[] } model KategoriBuku { @@ -2105,28 +2135,56 @@ model KategoriBuku { DataPerpustakaan DataPerpustakaan[] } +model PeminjamanBuku { + id String @id @default(cuid()) + nama String + noTelp String + alamat String + bukuId String + tanggalPinjam DateTime @default(now()) + batasKembali DateTime // tenggat waktu pengembalian + tanggalKembali DateTime? // diisi saat dikembalikan + status StatusPeminjaman @default(Dipinjam) + catatan String? // opsional, misal: kondisi buku + buku DataPerpustakaan @relation(fields: [bukuId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) +} + +enum StatusPeminjaman { + Dipinjam + Dikembalikan + Terlambat + Dibatalkan +} + // ========================================= USER ========================================= // model User { - id String @id @default(cuid()) - username String - nomor String @unique - role Role @relation(fields: [roleId], references: [id]) - roleId String @default("1") - instansi String? - UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll) - isActive Boolean @default(true) - lastLogin DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? + id String @id @default(cuid()) + username String + nomor String @unique + roleId String @default("2") + isActive Boolean @default(false) + sessionInvalid Boolean @default(false) + lastLogin DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + permissions Json? + sessions UserSession[] // ✅ Relasi one-to-many + role Role @relation(fields: [roleId], references: [id]) + menuAccesses UserMenuAccess[] + + @@map("users") } model Role { id String @id @default(cuid()) name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH description String? - permissions Json // Menyimpan permission dalam format JSON + permissions Json? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -2145,26 +2203,32 @@ model KodeOtp { otp Int } -// Tabel untuk menyimpan permission -model Permission { - id String @id @default(cuid()) - name String @unique - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +model UserSession { + id String @id @default(cuid()) + token String @db.Text // ✅ JWT bisa panjang + expiresAt DateTime // ✅ Ubah jadi expiresAt (konsisten) + active Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt - @@map("permissions") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String // ✅ HAPUS @unique - user bisa punya multiple sessions + + @@index([userId]) // ✅ Index untuk query cepat + @@index([token]) // ✅ Index untuk verify cepat + @@map("user_sessions") } -model UserSession { - id String @id @default(cuid()) - token String - expires DateTime? - active Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - User User @relation(fields: [userId], references: [id]) - userId String @unique +model UserMenuAccess { + id String @id @default(cuid()) + userId String + menuId String // ID menu (misal: "Landing Page", "Kesehatan") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id]) + + @@unique([userId, menuId]) // Satu user tidak bisa punya akses menu yang sama dua kali } // ========================================= DATA PENDIDIKAN ========================================= // diff --git a/prisma/seed.ts b/prisma/seed.ts index ab793335..bb52224d 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ import prisma from "@/lib/prisma"; import profilePejabatDesa from "./data/landing-page/profile/profile.json"; @@ -31,14 +32,14 @@ import sejarahDesa from "./data/desa/profile/sejarah_desa.json"; import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json"; import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json"; import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json"; -import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json"; -import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json"; -import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json"; -import kategoriBerita from "./data/kategori-berita.json"; +import pegawai from "./data/ekonomi/struktur-organisasi/pegawai-bumdes.json"; +import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json"; +import kategoriBerita from "./data/desa/berita/kategori-berita.json"; import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json"; import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json"; import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json"; import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json"; +import kategoriKegiatanData from "./data/lingkungan/gotong-royong/kategori-gotong-royong.json"; import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json"; import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json"; import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json"; @@ -54,89 +55,110 @@ import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-progr import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json"; import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json"; import roles from "./data/user/roles.json"; -import users from "./data/user/users.json"; import fileStorage from "./data/file-storage.json"; +import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json"; +import seedAssets from "./seed_assets"; +import users from "./data/user/users.json"; +import { safeSeedUnique } from "./safeseedUnique"; (async () => { - // =========== USER & ROLE =========== - // In your seed.ts - // =========== ROLES =========== console.log("🔄 Seeding roles..."); - for (const r of roles) { - await prisma.role.upsert({ - where: { id: r.id }, - update: { - name: r.name, - description: r.description, - permissions: r.permissions, - isActive: r.isActive, - }, - create: { - id: r.id, - name: r.name, - description: r.description, - permissions: r.permissions, - isActive: r.isActive, - }, - }); - } - console.log("✅ Roles seeded"); - // =========== USERS =========== + for (const r of roles) { + try { + // ✅ Destructure to remove permissions if exists + const { permissions, ...roleData } = r as any; + + await safeSeedUnique( + "role", + { name: roleData.name }, + { + id: roleData.id, + name: roleData.name, + description: roleData.description, + permissions: roleData.permissions || {}, // ✅ Include permissions + isActive: roleData.isActive, + } + ); + console.log(`✅ Seeded role -> ${roleData.name}`); + } catch (error: any) { + if (error.code === "P2002") { + console.warn(`⚠️ Role already exists (skipping): ${r.name}`); + } else { + console.error(`❌ Failed to seed role ${r.name}:`, error.message); + } + } + } + console.log("✅ Roles seeding completed"); + + // =========== USER =========== console.log("🔄 Seeding users..."); for (const u of users) { - // First verify the role exists - const roleExists = await prisma.role.findUnique({ - where: { id: u.roleId }, - }); + try { + // Verify role exists first + const roleExists = await prisma.role.findUnique({ + where: { id: u.roleId.toString() }, + select: { id: true }, // Only select id to minimize query + }); - if (!roleExists) { - console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`); - continue; + if (!roleExists) { + console.error( + `❌ Role with id ${u.roleId} not found for user ${u.username}` + ); + continue; + } + + await safeSeedUnique( + "user", + { id: u.id }, + { + username: u.username, + nomor: u.nomor, + roleId: u.roleId.toString(), + isActive: u.isActive, + sessionInvalid: false, + } + ); + console.log(`✅ Seeded user -> ${u.username}`); + } catch (error: any) { + if (error.code === "P2003") { + console.error( + `❌ Foreign key constraint failed for user ${u.username}: Role ${u.roleId} does not exist` + ); + } else { + console.error(`❌ Failed to seed user ${u.username}:`, error.message); + } } - - await prisma.user.upsert({ - where: { id: u.id }, - update: { - username: u.nama, - nomor: u.nomor, - roleId: u.roleId, - isActive: u.isActive, - }, - create: { - id: u.id, - username: u.nama, - nomor: u.nomor, - roleId: u.roleId, - isActive: u.isActive, - }, - }); } - console.log("✅ Users seeded"); + console.log("✅ Users seeding completed"); // =========== FILE STORAGE =========== console.log("🔄 Seeding file storage..."); for (const f of fileStorage) { - await prisma.fileStorage.upsert({ - where: { id: f.id }, - update: { - name: f.name, - realName: f.realName, - path: f.path, - mimeType: f.mimeType, - link: f.link, - category: f.category, - }, - create: { - id: f.id, - name: f.name, - realName: f.realName, - path: f.path, - mimeType: f.mimeType, - link: f.link, - category: f.category, - }, - }); + try { + await prisma.fileStorage.upsert({ + where: { id: f.id }, + update: { + name: f.name, + realName: f.realName, + path: f.path, + mimeType: f.mimeType, + link: f.link, + category: f.category, + }, + create: { + id: f.id, + name: f.name, + realName: f.realName, + path: f.path, + mimeType: f.mimeType, + link: f.link, + category: f.category, + }, + }); + } catch (error: any) { + console.error(`❌ Failed to seed file storage ${f.name}:`, error.message); + } } console.log("✅ File storage seeded"); // =========== LANDING PAGE =========== @@ -364,6 +386,7 @@ import fileStorage from "./data/file-storage.json"; jumlah: l.jumlah, }, create: { + id: l.id, name: l.name, jumlah: l.jumlah, }, @@ -554,15 +577,40 @@ import fileStorage from "./data/file-storage.json"; console.log("posisi organisasi berhasil"); // =========== PEGAWAI PPID =========== + console.log("🔄 Seeding pegawai PPID..."); const flattenedPegawai = pegawaiPPID.flat(); + + // Check for duplicate emails + const emails = new Set(); for (const p of flattenedPegawai) { - await prisma.pegawaiPPID.upsert({ - where: { id: p.id }, - update: p, - create: p, - }); + if (emails.has(p.email)) { + console.warn(`⚠️ Duplicate email found in pegawaiPPID: ${p.email}`); + } + emails.add(p.email); } - console.log("pegawai berhasil"); + + for (const p of flattenedPegawai) { + try { + await prisma.pegawaiPPID.upsert({ + where: { id: p.id }, + update: p, + create: p, + }); + console.log(`✅ Seeded pegawai PPID -> ${p.namaLengkap}`); + } catch (error: any) { + if (error.code === "P2002") { + console.warn( + `⚠️ Pegawai PPID with duplicate email (skipping): ${p.email}` + ); + } else { + console.error( + `❌ Failed to seed pegawai PPID ${p.namaLengkap}:`, + error.message + ); + } + } + } + console.log("✅ pegawai PPID seeding completed"); // =========== SUBMENU VISI MISI PPID =========== @@ -823,28 +871,36 @@ import fileStorage from "./data/file-storage.json"; } console.log("kategori produk success ..."); - for (const p of posisiOrganisasi) { - await prisma.posisiOrganisasi.upsert({ - where: { - id: p.id, - }, - update: { - nama: p.nama, - deskripsi: p.deskripsi, - hierarki: p.hierarki, - }, - create: { - id: p.id, - nama: p.nama, - deskripsi: p.deskripsi, - hierarki: p.hierarki, - }, + const flattenedPosisiBumdes = posisiOrganisasi.flat(); + + // ✅ Urutkan berdasarkan hierarki + const sortedPosisiBumdes = flattenedPosisiBumdes.sort( + (a, b) => a.hierarki - b.hierarki + ); + + for (const p of sortedPosisiBumdes) { + console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`); + + if (p.parentId) { + const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId); + if (!parentExists) { + console.warn( + `⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}` + ); + continue; + } + } + + await prisma.posisiOrganisasiBumDes.upsert({ + where: { id: p.id }, + update: p, + create: p, }); } - console.log("posisi organisasi success ..."); + console.log("posisi organisasi berhasil"); for (const p of pegawai) { - await prisma.pegawai.upsert({ + await prisma.pegawaiBumDes.upsert({ where: { id: p.id, }, @@ -873,26 +929,6 @@ import fileStorage from "./data/file-storage.json"; } console.log("pegawai success ..."); - for (const p of hubunganOrganisasi) { - await prisma.hubunganOrganisasi.upsert({ - where: { - atasanId_bawahanId: { - atasanId: p.atasanId, - bawahanId: p.bawahanId, - }, - }, - update: { - tipe: p.tipe, - }, - create: { - atasanId: p.atasanId, - bawahanId: p.bawahanId, - tipe: p.tipe, - }, - }); - } - console.log("hubungan organisasi success ..."); - for (const d of detailDataPengangguran) { await prisma.detailDataPengangguran.upsert({ where: { @@ -916,6 +952,30 @@ import fileStorage from "./data/file-storage.json"; } console.log("📊 detailDataPengangguran success ..."); + // =========== KATEGORI GOTONG ROYONG =========== + // Add IDs to the kategoriKegiatan data + const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({ + ...k, + id: `kategori-${index + 1}`, + })); + + for (const k of kategoriKegiatan) { + await prisma.kategoriKegiatan.upsert({ + where: { + id: k.id, + }, + update: { + nama: k.nama, + }, + create: { + id: k.id, + nama: k.nama, + }, + }); + } + + console.log("kategori kegiatan success ..."); + for (const e of tujuanEdukasiLingkungan) { await prisma.tujuanEdukasiLingkungan.upsert({ where: { @@ -1169,6 +1229,25 @@ import fileStorage from "./data/file-storage.json"; console.log( "✅ fasilitas bimbingan belajar desa seeded (editable later via UI)" ); + + for (const j of jenjangPendidikan) { + await prisma.jenjangPendidikan.upsert({ + where: { + id: j.id || undefined, + }, + update: { + nama: j.nama, + }, + create: { + nama: j.nama, + }, + }); + } + + console.log("✅ Jenjang Pendidikan seeded successfully"); + + // seed assets + await seedAssets(); })() .then(() => prisma.$disconnect()) .catch((e) => { diff --git a/prisma/seed_assets.ts b/prisma/seed_assets.ts new file mode 100644 index 00000000..f92c0d36 --- /dev/null +++ b/prisma/seed_assets.ts @@ -0,0 +1,118 @@ +// prisma/seedAssets.ts +import fs from "fs/promises"; +import path from "path"; +import sharp from "sharp"; +import fetch from "node-fetch"; +import AdmZip from "adm-zip"; +import prisma from "@/lib/prisma"; + +const UPLOADS_DIR = + process.env.WIBU_UPLOAD_DIR || path.join(process.cwd(), "uploads"); + +// --- Helper: deteksi kategori file --- +function detectCategory(filename: string): "image" | "document" | "other" { + const ext = path.extname(filename).toLowerCase(); + if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image"; + if ([".pdf", ".doc", ".docx"].includes(ext)) return "document"; + return "other"; +} + +// --- Helper: recursive walk dir --- +async function walkDir(dir: string, fileList: string[] = []): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + if (entry.name === "__MACOSX") continue; // skip folder sampah + await walkDir(fullPath, fileList); + } else { + if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; // skip file sampah + fileList.push(fullPath); + } + } + + return fileList; +} + +export default async function seedAssets() { + console.log("🚀 Seeding assets..."); + + // 1. Download zip + const url = + "https://cld-dkr-makuro-seafile.wibudev.com/f/ffd5a548a04f47939474/?dl=1"; + const res = await fetch(url); + if (!res.ok) throw new Error(`Gagal download assets: ${res.statusText}`); + const buffer = Buffer.from(await res.arrayBuffer()); + + // 2. Extract zip ke folder tmp + const extractDir = path.join(process.cwd(), "tmp_assets"); + await fs.rm(extractDir, { recursive: true, force: true }); + await fs.mkdir(extractDir, { recursive: true }); + + const zip = new AdmZip(buffer); + zip.extractAllTo(extractDir, true); + + // 3. Cari semua file valid (recursive) + const files = await walkDir(extractDir); + + // 4. Loop tiap file & simpan + for (const filePath of files) { + const entryName = path.basename(filePath); + const category = detectCategory(entryName); + + let finalName = entryName; + let mimeType = "application/octet-stream"; + let targetPath = ""; + + if (category === "image") { + const fileBaseName = path.parse(entryName).name; + finalName = `${fileBaseName}.webp`; + targetPath = path.join(UPLOADS_DIR, "images", finalName); + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await sharp(filePath).webp({ quality: 80 }).toFile(targetPath); + mimeType = "image/webp"; + } else if (category === "document") { + targetPath = path.join(UPLOADS_DIR, "documents", entryName); + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await fs.copyFile(filePath, targetPath); + mimeType = "application/pdf"; + } else { + targetPath = path.join(UPLOADS_DIR, "other", entryName); + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await fs.copyFile(filePath, targetPath); + } + + // 5. Simpan ke DB + await prisma.fileStorage.create({ + data: { + name: finalName, + realName: entryName, + path: targetPath, + mimeType, + link: `/uploads/${category}/${finalName}`, + category, + }, + }); + + console.log(`📂 saved: ${category}/${finalName}`); + } + + // 6. Cleanup + await fs.rm(extractDir, { recursive: true, force: true }); + + console.log("✅ Selesai seed assets!"); +} + +// --- Auto run kalau dipanggil langsung --- +if (import.meta.main) { + seedAssets() + .catch((err) => { + console.error("❌ Error seeding assets:", err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); +} diff --git a/public/beasiswa-siswa.png b/public/beasiswa-siswa.png new file mode 100644 index 00000000..ed40216d Binary files /dev/null and b/public/beasiswa-siswa.png differ diff --git a/public/perbekel.png b/public/perbekel.png index 7da92111..a940365b 100644 Binary files a/public/perbekel.png and b/public/perbekel.png differ diff --git a/src/app/admin/(dashboard)/_com/createEditor.tsx b/src/app/admin/(dashboard)/_com/createEditor.tsx index 7878e59a..86cf608c 100644 --- a/src/app/admin/(dashboard)/_com/createEditor.tsx +++ b/src/app/admin/(dashboard)/_com/createEditor.tsx @@ -7,6 +7,7 @@ import Underline from '@tiptap/extension-underline'; import TextAlign from '@tiptap/extension-text-align'; import Superscript from '@tiptap/extension-superscript'; import SubScript from '@tiptap/extension-subscript'; +import { useEffect } from 'react'; type CreateEditorProps = { value: string; @@ -32,6 +33,13 @@ export default function CreateEditor({ value, onChange }: CreateEditorProps) { }, }); + // 👇 Tambahkan efek untuk sinkronisasi value dari luar (resetForm) + useEffect(() => { + if (editor && value !== editor.getHTML()) { + editor.commands.setContent(value || ''); + } + }, [value, editor]); + return ( diff --git a/src/app/admin/(dashboard)/_com/editEditor.tsx b/src/app/admin/(dashboard)/_com/editEditor.tsx index 66317cc2..004a0dc0 100644 --- a/src/app/admin/(dashboard)/_com/editEditor.tsx +++ b/src/app/admin/(dashboard)/_com/editEditor.tsx @@ -47,6 +47,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) { editor.off('update', updateHandler); }; }, [editor, onChange]); + return ( diff --git a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx index 093a41df..c221672f 100644 --- a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx +++ b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx @@ -1,6 +1,6 @@ -'use client' +'use client'; -import { Box, rem, Select } from '@mantine/core'; +import { Box, Group, rem, Select, SelectProps } from '@mantine/core'; import { IconAmbulance, IconCash, @@ -25,7 +25,7 @@ import { IconTrophy, IconTruckFilled, IconBuilding, - IconAlertTriangle + IconAlertTriangle, } from '@tabler/icons-react'; const iconMap = { @@ -38,26 +38,26 @@ const iconMap = { scale: { label: 'Scale', icon: IconScale }, clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled }, trash: { label: 'Trash', icon: IconTrashFilled }, - lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco}, - sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled}, - ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp}, - mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, - rumah: {label: 'Rumah', icon: IconHome}, - pohon: {label: 'Pohon', icon: IconTree}, - air: {label: 'Air', icon: IconDroplet}, - bantuan: {label: 'Bantuan', icon: IconCash}, - pelatihan: {label: 'Pelatihan', icon: IconSchool}, - subsidi: {label: 'Subsidi', icon: IconShoppingCart}, - layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital}, - polisi: {label: 'Polisi', icon: IconShieldFilled}, - ambulans: {label: 'Ambulans', icon: IconAmbulance}, - pemadam: {label: 'Pemadam', icon: IconFiretruck}, - rumahSakit: {label: 'Rumah Sakit', icon: IconHospital}, - bangunan: {label: 'Bangunan', icon: IconBuilding}, - darurat: {label: 'Darurat', icon: IconAlertTriangle}, + lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco }, + sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled }, + ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp }, + mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled }, + rumah: { label: 'Rumah', icon: IconHome }, + pohon: { label: 'Pohon', icon: IconTree }, + air: { label: 'Air', icon: IconDroplet }, + bantuan: { label: 'Bantuan', icon: IconCash }, + pelatihan: { label: 'Pelatihan', icon: IconSchool }, + subsidi: { label: 'Subsidi', icon: IconShoppingCart }, + layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital }, + polisi: { label: 'Polisi', icon: IconShieldFilled }, + ambulans: { label: 'Ambulans', icon: IconAmbulance }, + pemadam: { label: 'Pemadam', icon: IconFiretruck }, + rumahSakit: { label: 'Rumah Sakit', icon: IconHospital }, + bangunan: { label: 'Bangunan', icon: IconBuilding }, + darurat: { label: 'Darurat', icon: IconAlertTriangle }, }; -type IconKey = keyof typeof iconMap; +export type IconKey = keyof typeof iconMap; const iconList = Object.entries(iconMap).map(([value, data]) => ({ value, @@ -67,44 +67,52 @@ const iconList = Object.entries(iconMap).map(([value, data]) => ({ export default function SelectIconProgramEdit({ onChange, value, + ...props }: { - onChange: (value: IconKey) => void; - value: IconKey; -}) { - const IconComponent = iconMap[value]?.icon || null; - + onChange: (value: IconKey | '') => void; + value: IconKey | ''; +} & Omit) { return ( - setFormData({ ...formData, kategoriBeritaId: val || "" }) - } + onChange={(val) => handleChange("kategoriBeritaId", val || "")} label="Kategori" placeholder="Pilih kategori" data={ @@ -236,18 +201,138 @@ function EditBerita() { error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined} /> + + + Deskripsi Singkat + + + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } + /> + + + + {/* Upload Gambar */} + + + Gambar Berita + + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => + toast.error("File tidak valid, gunakan format gambar") + } + maxSize={5 * 1024 ** 2} + accept={{ "image/*": [] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + + + + + + {previewImage && ( + + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + + + {/* Konten */} + + + Konten + + + setFormData((prev) => ({ ...prev, content: htmlContent })) + } + /> + + + {/* Action */} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx index 2ffcb31c..54f53634 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx @@ -1,14 +1,14 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; -import colors from '@/con/colors'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; +import colors from '@/con/colors'; function DetailBerita() { const beritaState = useProxy(stateDashboardBerita); @@ -80,7 +80,7 @@ function DetailBerita() { Deskripsi - {data.deskripsi || '-'} + @@ -111,7 +111,6 @@ function DetailBerita() { {/* Action Button */} - - - - diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx index 3100163a..9a1d5acb 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx @@ -14,7 +14,8 @@ import { Text, TextInput, Title, - Tooltip, + Loader, + ActionIcon } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useShallowEffect } from '@mantine/hooks'; @@ -29,6 +30,7 @@ export default function CreateBerita() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); useShallowEffect(() => { beritaState.kategoriBerita.findMany.load(); @@ -47,42 +49,48 @@ export default function CreateBerita() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + beritaState.berita.create.form.imageId = uploaded.id; + + await beritaState.berita.create.create(); + + resetForm(); + router.push('/admin/desa/berita/list-berita'); + } catch (error) { + console.error('Error creating berita:', error); + toast.error('Terjadi kesalahan saat membuat berita'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - beritaState.berita.create.form.imageId = uploaded.id; - - await beritaState.berita.create.create(); - - resetForm(); - router.push('/admin/desa/berita/list-berita'); }; return ( {/* Header dengan tombol kembali */} - - - + Tambah Berita @@ -131,12 +139,17 @@ export default function CreateBerita() { required /> - (beritaState.berita.create.form.deskripsi = e.target.value)} - /> + + + Deskripsi Singkat + + { + beritaState.berita.create.form.deskripsi = htmlContent; + }} + /> + @@ -152,7 +165,7 @@ export default function CreateBerita() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -173,7 +186,7 @@ export default function CreateBerita() { {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -202,6 +235,17 @@ export default function CreateBerita() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx index 47b628d8..38bd0ffa 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; @@ -68,7 +67,6 @@ function ListBerita({ search }: { search: string }) { Daftar Berita - - diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx index 3b08fbea..ac66a2df 100644 --- a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx @@ -13,8 +13,7 @@ import { Stack, Text, TextInput, - Title, - Tooltip, + Title } from "@mantine/core"; import { useShallowEffect } from "@mantine/hooks"; import { IconSearch, IconTrash, IconX } from "@tabler/icons-react"; @@ -103,7 +102,6 @@ export default function ListImage() { - - diff --git a/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx index 803a6884..43a029bb 100644 --- a/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { IconPhoto, IconVideo } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconPhoto, IconVideo } from '@tabler/icons-react'; function LayoutTabsGallery({ children }: { children: React.ReactNode }) { const router = useRouter() @@ -14,15 +14,13 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) { label: "Foto", value: "foto", href: "/admin/desa/gallery/foto", - icon: , - tooltip: "Kelola foto-foto galeri desa" + icon: }, { label: "Video", value: "video", href: "/admin/desa/gallery/video", - icon: , - tooltip: "Kelola video galeri desa" + icon: }, ]; @@ -70,25 +68,18 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts b/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts index 26fa1175..afbca78b 100644 --- a/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts +++ b/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts @@ -1,31 +1,11 @@ +// export function convertYoutubeUrlToEmbed(url: string) { +// const videoIdMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/); +// return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null; +// } + export function convertYoutubeUrlToEmbed(url: string) { - const videoIdMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/); + const videoIdMatch = url.match( + /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/ + ); return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null; -} - - - - - - - - - -// (url: string): string | null { -// const watchRegex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/; -// const shortRegex = /(?:https?:\/\/)?youtu\.be\/([^?]+)/; - -// const matchWatch = url.match(watchRegex); -// const matchShort = url.match(shortRegex); - -// if (matchWatch) { -// return `https://www.youtube.com/embed/${matchWatch[1]}`; -// } - -// if (matchShort) { -// return `https://www.youtube.com/embed/${matchShort[1]}`; -// } - -// return null; -// } - \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx index df68af39..21387eb2 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx @@ -4,6 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import colors from '@/con/colors'; import { + ActionIcon, Box, Button, Group, @@ -11,11 +12,11 @@ import { Stack, TextInput, Title, - Tooltip + Loader } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; +import { IconArrowBack, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils'; @@ -25,23 +26,38 @@ function EditVideo() { const videoState = useProxy(stateGallery.video); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + linkVideo: "", + }); + const [formData, setFormData] = useState({ name: '', deskripsi: '', linkVideo: '', }); + // load data video sekali saat id ada useEffect(() => { const loadVideo = async () => { const id = params?.id as string; if (!id) return; + try { const data = await videoState.update.load(id); if (data) { setFormData({ - name: data.name || '', - deskripsi: data.deskripsi || '', - linkVideo: data.linkVideo || '', + name: data.name ?? '', + deskripsi: data.deskripsi ?? '', + linkVideo: data.linkVideo ?? '', + }); + setOriginalData({ + name: data.name ?? '', + deskripsi: data.deskripsi ?? '', + linkVideo: data.linkVideo ?? '', }); } } catch (error) { @@ -49,42 +65,69 @@ function EditVideo() { toast.error('Gagal memuat data video'); } }; + loadVideo(); }, [params?.id]); - const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo); + const handleChange = useCallback( + (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }, + [] + ); + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + linkVideo: originalData.linkVideo, + }); + toast.info("Form dikembalikan ke data awal"); + }; const handleSubmit = async () => { - const converted = convertYoutubeUrlToEmbed(formData.linkVideo); - if (!converted) { - toast.error("Link YouTube tidak valid. Pastikan formatnya benar."); - return; - } - try { - videoState.update.form = { - ...videoState.update.form, - name: formData.name, - deskripsi: formData.deskripsi, - linkVideo: formData.linkVideo, - }; - await videoState.update.update(); - toast.success('Video berhasil diperbarui!'); - router.push('/admin/desa/gallery/video'); + setIsSubmitting(true); + const converted = convertYoutubeUrlToEmbed(formData.linkVideo); + if (!converted) { + toast.error("Link YouTube tidak valid. Pastikan formatnya benar."); + return; + } + + try { + videoState.update.form = { + name: formData.name, + deskripsi: formData.deskripsi, + linkVideo: formData.linkVideo, + }; + await videoState.update.update(); + toast.success('Video berhasil diperbarui!'); + router.push('/admin/desa/gallery/video'); + } catch (error) { + console.error('Error updating video:', error); + toast.error('Terjadi kesalahan saat memperbarui video'); + } } catch (error) { console.error('Error updating video:', error); toast.error('Terjadi kesalahan saat memperbarui video'); + } finally { + setIsSubmitting(false); } }; + const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo); + return ( - - - + Edit Video @@ -103,7 +146,7 @@ function EditVideo() { label="Judul Video" placeholder="Masukkan judul video" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange('name', e.currentTarget.value)} required /> @@ -112,11 +155,11 @@ function EditVideo() { label="Link Video YouTube" placeholder="https://www.youtube.com/watch?v=abc123" value={formData.linkVideo} - onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })} + onChange={(e) => handleChange('linkVideo', e.currentTarget.value)} required /> {embedLink && ( - + + /> + { + setFormData({ + ...formData, + linkVideo: '', + }); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -135,11 +198,22 @@ function EditVideo() { setFormData({ ...formData, deskripsi: val })} + onChange={(val) => handleChange('deskripsi', val)} /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx index 52bef7cd..598c745b 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -102,6 +102,7 @@ function DetailVideo() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> ) : ( Tidak ada deskripsi @@ -110,7 +111,6 @@ function DetailVideo() { {/* Tombol Aksi */} - - - - diff --git a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx index 9194a15e..3e53d4cd 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx @@ -3,6 +3,7 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import colors from '@/con/colors'; import { + ActionIcon, Box, Button, Group, @@ -11,9 +12,9 @@ import { Text, TextInput, Title, - Tooltip, + Loader } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; +import { IconArrowBack, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { toast } from 'react-toastify'; @@ -25,6 +26,7 @@ function CreateVideo() { const router = useRouter(); const [link, setLink] = useState(''); const embedLink = convertYoutubeUrlToEmbed(link); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { videoState.create.form = { @@ -36,31 +38,37 @@ function CreateVideo() { }; const handleSubmit = async () => { - if (!embedLink) { - toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); - return; - } + try { + setIsSubmitting(true); + if (!embedLink) { + toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); + return; + } - videoState.create.form.linkVideo = embedLink; - await videoState.create.create(); - resetForm(); - router.push('/admin/desa/gallery/video'); + videoState.create.form.linkVideo = embedLink; + await videoState.create.create(); + resetForm(); + router.push('/admin/desa/gallery/video'); + } catch (error) { + console.error("Error creating video:", error); + toast.error("Terjadi kesalahan saat menambahkan video"); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header Back Button + Title */} - - - + Tambah Video @@ -98,7 +106,7 @@ function CreateVideo() { {/* Preview Video */} {embedLink && ( - + + /> + { + setLink(''); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -128,6 +153,17 @@ function CreateVideo() { {/* Button Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/gallery/video/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/page.tsx index 232204ff..384fb7d9 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -74,7 +73,6 @@ function ListVideo({ search }: { search: string }) { Daftar Video - - diff --git a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx index 79fe9452..0ee0d2e2 100644 --- a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx @@ -1,17 +1,17 @@ -'use client' /* eslint-disable react-hooks/exhaustive-deps */ +'use client' import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import colors from '@/con/colors'; import { - Box, - Button, - Group, - Paper, - Select, - Stack, - TextInput, - Title, - Tooltip + Box, + Button, + Group, + Loader, + Paper, + Select, + Stack, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -20,159 +20,199 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function EditAjukanPermohonan() { - const router = useRouter(); - const params = useParams(); - const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan); + const router = useRouter(); + const params = useParams(); + const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan); - const [formData, setFormData] = useState({ - nama: stateAjukan.edit.form.nama, - nik: stateAjukan.edit.form.nik, - alamat: stateAjukan.edit.form.alamat, - nomorKk: stateAjukan.edit.form.nomorKk, - kategoriId: stateAjukan.edit.form.kategoriId, - }); + const [isSubmitting, setIsSubmitting] = useState(false); - useEffect(() => { - stateLayananDesa.suratKeterangan.findManyAll.load(); - const loadAjukan = async () => { - const id = params?.id as string; - if (!id) return; + const [originalData, setOriginalData] = useState({ + nama: "", + nik: "", + alamat: "", + nomorKk: "", + kategoriId: "", + }); - try { - const data = await stateAjukan.edit.load(id); - if (data) { - setFormData({ - nama: data.nama || '', - nik: data.nik || '', - alamat: data.alamat || '', - nomorKk: data.nomorKk || '', - kategoriId: data.kategoriId || '', - }); - } - } catch (error) { - console.error('Error loading ajukan:', error); - toast.error('Gagal memuat data ajukan'); - } - }; + // State lokal form + const [formData, setFormData] = useState({ + nama: '', + nik: '', + alamat: '', + nomorKk: '', + kategoriId: '', + }); - loadAjukan(); - }, [params?.id]); + // Load data awal + useEffect(() => { + stateLayananDesa.suratKeterangan.findManyAll.load(); - const handleSubmit = async () => { - try { - stateAjukan.edit.form = { - ...stateAjukan.edit.form, - ...formData, - }; - toast.success('Ajukan berhasil diperbarui!'); - router.push('/admin/desa/layanan/ajukan_permohonan'); - } catch (error) { - console.error('Error updating ajukan:', error); - toast.error('Terjadi kesalahan saat memperbarui ajukan'); + const loadAjukan = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await stateAjukan.edit.load(id); + if (data) { + setFormData({ + nama: data.nama || '', + nik: data.nik || '', + alamat: data.alamat || '', + nomorKk: data.nomorKk || '', + kategoriId: data.kategoriId || '', + }); + setOriginalData({ + nama: data.nama || '', + nik: data.nik || '', + alamat: data.alamat || '', + nomorKk: data.nomorKk || '', + kategoriId: data.kategoriId || '', + }); } + } catch (error) { + console.error('Error loading ajukan:', error); + toast.error('Gagal memuat data ajukan'); + } }; - return ( - - {/* Back Button */} - - - - - - Edit Ajukan Permohonan - - + loadAjukan(); + }, [params?.id]); - { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + nik: originalData.nik, + alamat: originalData.alamat, + nomorKk: originalData.nomorKk, + kategoriId: originalData.kategoriId, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + stateAjukan.edit.form = { + ...stateAjukan.edit.form, + ...formData, + }; + toast.success('Ajukan berhasil diperbarui!'); + router.push('/admin/desa/layanan/ajukan_permohonan'); + } catch (error) { + console.error('Error updating ajukan:', error); + toast.error('Terjadi kesalahan saat memperbarui ajukan'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {/* Back Button */} + + + + 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 - /> - - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } export default EditAjukanPermohonan; diff --git a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx index 808c0229..c53534c0 100644 --- a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx @@ -3,14 +3,13 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import colors from '@/con/colors'; import { - Box, - Button, - Group, - Paper, - Skeleton, - Stack, - Text, - Tooltip + Box, + Button, + Group, + Paper, + Skeleton, + Stack, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -79,7 +78,7 @@ function DetailAjukanPermohonan() { Nama - + {data?.nama || '-'} @@ -97,7 +96,7 @@ function DetailAjukanPermohonan() { Alamat - + {data?.alamat || '-'} @@ -121,7 +120,6 @@ function DetailAjukanPermohonan() { - - - - diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx new file mode 100644 index 00000000..a425a358 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx @@ -0,0 +1,192 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' + +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditPelayananPendudukNonPermanent() { + const router = useRouter(); + const params = useParams(); + const statePendudukNonPermanent = useProxy( + stateLayananDesa.pelayananPendudukNonPermanen + ); + + const [formData, setFormData] = useState({ + name: '', + deskripsi: '', + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + }); + + + // Load data sekali dari backend + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await statePendudukNonPermanent.update.load(id); + if (data) { + setFormData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + }); + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + }); + } + } catch (error) { + console.error('Error loading data:', error); + toast.error('Gagal memuat data pelayanan penduduk non permanent'); + } + }; + + loadData(); + }, [params?.id]); + + const handleChange = + (field: keyof typeof formData) => + (value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + if (!statePendudukNonPermanent.findById.data) return; + + // Update global state hanya di submit + const updated = { + ...statePendudukNonPermanent.findById.data, + name: formData.name, + deskripsi: formData.deskripsi, + }; + + await statePendudukNonPermanent.update.update(updated); + router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent'); + } catch (error) { + console.error('Error updating data:', error); + toast.error('Gagal memuat data pelayanan penduduk non permanent'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + + Edit Pelayanan Penduduk Non Permanent + + + + + + Edit Pelayanan Penduduk Non Permanent + + {/* Nama Field */} + handleChange('name')(e.target.value)} + required + /> + + {/* Deskripsi Field */} + + + Deskripsi + + + + + {/* Submit Button */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + + ); +} + +export default EditPelayananPendudukNonPermanent; diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx deleted file mode 100644 index eed1a9a3..00000000 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx +++ /dev/null @@ -1,125 +0,0 @@ -'use client' -/* eslint-disable react-hooks/exhaustive-deps */ -import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; -import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; -import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; - -function EditPelayananPendudukNonPermanent() { - const router = useRouter(); - const params = useParams() - const statePendudukNonPermanent = useProxy(stateLayananDesa.pelayananPendudukNonPermanen) - const [formData, setFormData] = useState({ - name: statePendudukNonPermanent.findById.data?.name || '', - deskripsi: statePendudukNonPermanent.findById.data?.deskripsi || '', - }) - - useEffect(() => { - const loadPelayananPerizinan = async () => { - const id = params?.id as string; - if (!id) return; - try { - const data = await statePendudukNonPermanent.update.load(id); - if (data) { - setFormData({ - name: data.name || '', - deskripsi: data.deskripsi || '', - }); - } - } catch (error) { - console.error("Error loading pelayanan perizinan berusaha:", error); - toast.error("Gagal memuat data pelayanan perizinan berusaha"); - } - }; - loadPelayananPerizinan(); - }, [params?.id]); - - const handleSubmit = async () => { - if (statePendudukNonPermanent.findById.data) { - statePendudukNonPermanent.findById.data.name = formData.name; - statePendudukNonPermanent.findById.data.deskripsi = formData.deskripsi; - statePendudukNonPermanent.update.update(statePendudukNonPermanent.findById.data) - } - router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent') - } - return ( - - - - - - - - Edit Pelayanan Penduduk Non Permanent - - - - - - Edit Pelayanan Penduduk Non Permanent - - {/* Nama Field */} - - setFormData({ ...formData, name: e.target.value }) - } - required - /> - - {/* Posisi Field */} - - - Deskripsi - - { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - }} - /> - - - {/* Submit Button */} - - - - - - - - - - ); -} - -export default EditPelayananPendudukNonPermanent; diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx index 0d89b55b..cfa734ca 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx @@ -11,8 +11,7 @@ import { Skeleton, Stack, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit } from '@tabler/icons-react'; @@ -27,7 +26,7 @@ function PelayananPendudukNonPermanent() { ); useShallowEffect(() => { - pelayananPendudukNonPermanen.findById.load('1'); + pelayananPendudukNonPermanen.findById.load('edit'); }, []); if (!pelayananPendudukNonPermanen.findById.data) { @@ -51,7 +50,6 @@ function PelayananPendudukNonPermanent() { - - @@ -91,6 +88,7 @@ function PelayananPendudukNonPermanent() { ta="justify" fz={{ base: '1rem', md: '1.2rem' }} dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx new file mode 100644 index 00000000..9fdc6c56 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx @@ -0,0 +1,211 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' + +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + Paper, + Skeleton, + Stack, + TextInput, + Title +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditPelayananPerizinanBerusaha() { + const router = useRouter(); + const params = useParams<{ id: string }>(); + const id = params?.id; // ini langsung string + const state = useProxy(stateLayananDesa.pelayananPerizinanBerusaha); + + const [loading, setLoading] = useState(true); + const [formData, setFormData] = useState({ + id: '', + name: '', + deskripsi: '', + link: '', + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + id: '', + name: '', + deskripsi: '', + link: '', + }); + + // Load data detail + useEffect(() => { + if (!id) { + toast.error("ID tidak valid"); + return; + } + + const loadData = async () => { + try { + setLoading(true); + const data = await state.findById.load(id); + if (data) { + setFormData({ + id: data.id, + name: data.name || "", + deskripsi: data.deskripsi || "", + link: data.link || "", + }); + setOriginalData({ + id: data.id, + name: data.name || "", + deskripsi: data.deskripsi || "", + link: data.link || "", + }); + } else { + toast.error("Data tidak ditemukan"); + } + } catch (error) { + console.error("Error loading data:", error); + toast.error("Gagal memuat data"); + } finally { + setLoading(false); + } + }; + + loadData(); + }, [id]); + + + const handleChange = + (field: keyof typeof formData) => + (value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + id: originalData.id, + name: originalData.name, + deskripsi: originalData.deskripsi, + link: originalData.link, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + await state.update.update(formData); + router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha'); + } catch (error) { + console.error('Error updating pelayanan perizinan berusaha:', error); + toast.error('Terjadi kesalahan saat update data'); + } finally { + setIsSubmitting(false); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + {/* Header */} + + + + Edit Pelayanan Perizinan Berusaha + + + + {/* Form */} + + + Edit Pelayanan Perizinan Berusaha + + handleChange('name')(e.target.value)} + required + /> + + handleChange('link')(e.target.value)} + /> + + + Deskripsi + + + + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + + ); +} + +export default EditPelayananPerizinanBerusaha; diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/edit/page.tsx deleted file mode 100644 index 05488b2d..00000000 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/edit/page.tsx +++ /dev/null @@ -1,134 +0,0 @@ -'use client' -/* eslint-disable react-hooks/exhaustive-deps */ -import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; -import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; -import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; - -function EditPelayananPerizinanBerusaha() { - const router = useRouter(); - const params = useParams() - const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha) - - const [formData, setFormData] = useState({ - name: statePerizinanBerusaha.findById.data?.name || '', - deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '', - link: statePerizinanBerusaha.findById.data?.link || '', - }) - - useEffect(() => { - const loadPelayananPerizinan = async () => { - const id = params?.id as string; - if (!id) return; - try { - const data = await statePerizinanBerusaha.update.load(id); - if (data) { - setFormData({ - name: data.name || '', - deskripsi: data.deskripsi || '', - link: data.link || '', - }); - } - } catch (error) { - console.error("Error loading pelayanan perizinan berusaha:", error); - toast.error("Gagal memuat data pelayanan perizinan berusaha"); - } - }; - loadPelayananPerizinan(); - }, [params?.id]); - - const handleSubmit = async () => { - if (statePerizinanBerusaha.findById.data) { - statePerizinanBerusaha.findById.data.name = formData.name; - statePerizinanBerusaha.findById.data.deskripsi = formData.deskripsi; - statePerizinanBerusaha.findById.data.link = formData.link; - statePerizinanBerusaha.update.update(statePerizinanBerusaha.findById.data) - } - router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha') - } - - return ( - - - {/* Header Section */} - - - - - - Edit Pelayanan Perizinan Berusaha - - - - {/* Form Section */} - - - Edit Pelayanan Perizinan Berusaha - - {/* Nama Field */} - setFormData({ ...formData, name: e.target.value })} - required - /> - - {/* Link Field */} - setFormData({ ...formData, link: e.target.value })} - /> - - {/* Deskripsi Field */} - - Deskripsi - setFormData({ ...formData, deskripsi: val })} - /> - - - {/* Action Buttons */} - - - - - - - - - - ); -} - -export default EditPelayananPerizinanBerusaha; diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx index 2c6a41fe..a1cac551 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; import { @@ -15,39 +16,61 @@ import { StepperCompleted, StepperStep, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconEdit } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import stateLayananDesa from '../../../_state/desa/layananDesa'; +import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; +import stateLayananDesa from '../../../_state/desa/layananDesa'; function PerizinanBerusaha() { const router = useRouter(); const pelayananPerizinanBerusaha = useProxy( stateLayananDesa.pelayananPerizinanBerusaha ); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); const [active, setActive] = useState(1); const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current)); const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); - useShallowEffect(() => { - pelayananPerizinanBerusaha.findById.load('1'); + useEffect(() => { + const loadData = async () => { + try { + setLoading(true); + // You should get the ID from your router query or params + const id = 'edit'; // Replace with actual ID or get from URL params + await pelayananPerizinanBerusaha.findById.load(id); + } catch (err) { + setError('Gagal memuat data'); + console.error('Error:', err); + } finally { + setLoading(false); + } + }; + + loadData(); }, []); - if (!pelayananPerizinanBerusaha.findById.data) { + if (loading) { return ( - + ); } + if (error || !pelayananPerizinanBerusaha.findById.data) { + return ( +
+ {error || 'Data tidak ditemukan'} +
+ ); + } + const data = pelayananPerizinanBerusaha.findById.data; return ( @@ -61,7 +84,6 @@ function PerizinanBerusaha() { - - @@ -101,6 +122,7 @@ function PerizinanBerusaha() { ta="justify" fz={{ base: '1rem', md: '1.2rem' }} dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> >; + preview: string | null; + setPreview: React.Dispatch>; +} + +// 🔹 File Uploader Component +const FileUploader: React.FC = ({ + title, + file, + setFile, + preview, + setPreview +}) => { + const handleDrop = (files: File[]) => { + const selected = files[0]; + if (selected) { + setFile(selected); + setPreview(URL.createObjectURL(selected)); + } + }; + + const handleRemove = () => { + setPreview(null); + setFile(null); + }; + + return ( + + + {title} + + toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format .png, .jpg, .jpeg, .webp + + + + + + {preview && ( + + + + + + + )} + + ); +}; + +// 🔹 Main Component function EditSuratKeterangan() { const router = useRouter(); const params = useParams(); - const stateSurat = useProxy(stateLayananDesa.suratKeterangan); - const [previewImage, setPreviewImage] = useState(null); - const [previewImage2, setPreviewImage2] = useState(null); + // 🧩 State + const [formData, setFormData] = useState({ + name: '', + deskripsi: '', + imageId: '', + image2Id: '', + imageUrl: '', + image2Url: '', + }); + const [originalData, setOriginalData] = useState(formData); const [file, setFile] = useState(null); const [file2, setFile2] = useState(null); - const [formData, setFormData] = useState({ - name: stateSurat.edit.form.name, - deskripsi: stateSurat.edit.form.deskripsi, - imageId: stateSurat.edit.form.imageId, - image2Id: stateSurat.edit.form.image2Id, - }); + const [previewImage, setPreviewImage] = useState(null); + const [previewImage2, setPreviewImage2] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + // 🧭 Load Initial Data useEffect(() => { const loadSurat = async () => { const id = params?.id as string; if (!id) return; try { - const data = await stateSurat.edit.load(id); - if (data) { - setFormData({ - name: data.name || '', - deskripsi: data.deskripsi || '', - imageId: data.imageId || '', - image2Id: data.image2Id || '', - }); + const data = await stateLayananDesa.suratKeterangan.edit.load(id); + if (!data) return; - setPreviewImage(data.image?.link || null); - setPreviewImage2(data.image2?.link || null); - } + const mapped: FormData = { + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + image2Id: data.image2Id || '', + imageUrl: data.image?.link || '', + image2Url: data.image2?.link || '' + }; + + setFormData(mapped); + setOriginalData(mapped); + + if (data.image?.link) setPreviewImage(data.image.link); + if (data.image2?.link) setPreviewImage2(data.image2.link); } catch (error) { console.error('Error loading surat:', error); toast.error('Gagal memuat data surat'); @@ -66,50 +182,99 @@ function EditSuratKeterangan() { loadSurat(); }, [params?.id]); - const handleSubmit = async () => { + // 📤 Upload File Helper + const uploadFile = async (file: File): Promise => { try { - stateSurat.edit.form = { - ...stateSurat.edit.form, - ...formData, - }; + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + const uploaded = res.data?.data; + return uploaded?.id || null; + } catch (error) { + console.error('Error uploading file:', error); + return null; + } + }; + // 🔁 Reset Form + const handleResetForm = () => { + setFormData(originalData); + setPreviewImage(originalData.imageUrl || null); + setPreviewImage2(originalData.image2Url || null); + setFile(null); + setFile2(null); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit Handler + const handleSubmit = useCallback(async () => { + try { + setIsSubmitting(true); + + // ✅ Access original state directly (not proxy) + const originalState = stateLayananDesa.suratKeterangan; + + // Update form data properties individually + originalState.edit.form.name = formData.name; + originalState.edit.form.deskripsi = formData.deskripsi; + originalState.edit.form.imageId = formData.imageId; + originalState.edit.form.image2Id = formData.image2Id; + + // Upload file 1 if exists if (file) { - const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); - const uploaded = res.data?.data; - if (!uploaded?.id) return toast.error('Gagal upload gambar'); - stateSurat.edit.form.imageId = uploaded.id; + const uploadedId = await uploadFile(file); + if (!uploadedId) { + toast.error('Gagal upload gambar pertama'); + return; + } + originalState.edit.form.imageId = uploadedId; } + // Upload file 2 if exists if (file2) { - const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name }); - const uploaded = res.data?.data; - if (!uploaded?.id) return toast.error('Gagal upload gambar'); - stateSurat.edit.form.image2Id = uploaded.id; + const uploadedId = await uploadFile(file2); + if (!uploadedId) { + toast.error('Gagal upload gambar kedua'); + return; + } + originalState.edit.form.image2Id = uploadedId; } - await stateSurat.edit.update(); + // Submit update + await originalState.edit.update(); toast.success('Surat berhasil diperbarui!'); router.push('/admin/desa/layanan/pelayanan_surat_keterangan'); } catch (error) { console.error('Error updating surat:', error); toast.error('Terjadi kesalahan saat memperbarui surat'); + } finally { + setIsSubmitting(false); } + }, [formData, file, file2, router]); + + // 📝 Form Field Handlers + const handleNameChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ ...prev, name: e.target.value })); + }; + + const handleDeskripsiChange = (html: string) => { + setFormData(prev => ({ ...prev, deskripsi: html })); }; return ( - {/* Back Button */} + {/* Header */} - - - + Edit Surat Keterangan + {/* Form */} + {/* Nama Surat */} setFormData({ ...formData, name: e.target.value })} + onChange={handleNameChange} required /> + {/* Deskripsi */} Konten setFormData({ ...formData, deskripsi: htmlContent })} + onChange={handleDeskripsiChange} /> - {/* Upload Gambar 1 */} - - - Gambar Konten Pelayanan - - { - const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error('File tidak valid, gunakan format gambar')} - maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} - radius="md" - p="xl" - > - - - - - - - - - - - - - Seret gambar atau klik untuk memilih file - - - Maksimal 5MB, format gambar wajib - - - - + {/* Gambar 1 */} + - {previewImage && ( - - - - )} - - - {/* Upload Gambar 2 */} - - - Gambar Alur Pelayanan Surat - - { - const selectedFile = files[0]; - if (selectedFile) { - setFile2(selectedFile); - setPreviewImage2(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error('File tidak valid, gunakan format gambar')} - maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} - radius="md" - p="xl" - > - - - - - - - - - - - - - Seret gambar atau klik untuk memilih file - - - Maksimal 5MB, format gambar wajib - - - - - - {previewImage2 && ( - - - - )} - + {/* Gambar 2 */} + + {/* Action Buttons */} + @@ -271,4 +352,4 @@ function EditSuratKeterangan() { ); } -export default EditSuratKeterangan; +export default EditSuratKeterangan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx index 2da12ed5..4c911ca3 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx @@ -10,8 +10,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -95,6 +94,7 @@ function DetailSuratKeterangan() { dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-', }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -141,7 +141,6 @@ function DetailSuratKeterangan() { - - - - diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx index 2af7efc7..9c065b52 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx @@ -5,16 +5,17 @@ import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -28,6 +29,7 @@ function CreateSuratKeterangan() { const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null); const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateSurat.create.form = { @@ -46,6 +48,7 @@ function CreateSuratKeterangan() { } try { + setIsSubmitting(true); // Upload gambar utama const res1 = await ApiFetch.api.fileStorage.create.post({ file: previewImage.file, @@ -78,6 +81,8 @@ function CreateSuratKeterangan() { } catch (error) { console.error('Error creating surat keterangan:', error); toast.error('Terjadi kesalahan saat menambahkan surat keterangan'); + } finally { + setIsSubmitting(false); } }; @@ -85,11 +90,9 @@ function CreateSuratKeterangan() { {/* Header */} - - - + Tambah Surat Keterangan @@ -108,7 +111,7 @@ function CreateSuratKeterangan() { (stateSurat.create.form.name = val.target.value)} - label={Nama Surat Keterangan} + label="Nama Surat Keterangan" placeholder="Masukkan nama surat keterangan" required /> @@ -143,7 +146,7 @@ function CreateSuratKeterangan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -164,7 +167,7 @@ function CreateSuratKeterangan() { {previewImage && ( - + + { + setPreviewImage(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -193,7 +213,7 @@ function CreateSuratKeterangan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -214,7 +234,7 @@ function CreateSuratKeterangan() { {previewImage2 ? ( - + + { + setPreviewImage2(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : ( @@ -232,6 +269,17 @@ function CreateSuratKeterangan() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx index db80a721..2b90ada8 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx @@ -17,8 +17,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -82,7 +81,6 @@ function ListSuratKeterangan({ search }: { search: string }) { List Surat Keterangan - -
@@ -119,6 +116,7 @@ function ListSuratKeterangan({ search }: { search: string }) { diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx index a80f7bb1..08ae9730 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx @@ -6,42 +6,54 @@ import { Box, Button, Group, + Loader, Paper, Stack, - Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; -import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; function EditPelayananTelunjukSakti() { const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - name: stateTelunjukDesa.edit.form.name, - deskripsi: stateTelunjukDesa.edit.form.deskripsi, - link: stateTelunjukDesa.edit.form.link, + name: '', + deskripsi: '', + link: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + link: '', + }); + + // Load data awal hanya sekali (pas ada id) useEffect(() => { - const loadPelayananTelunjukSakti = async () => { + const loadData = async () => { const id = params?.id as string; if (!id) return; + try { const data = await stateTelunjukDesa.edit.load(id); if (data) { setFormData({ - name: data.name || '', - deskripsi: data.deskripsi || '', - link: data.link || '', + name: data.name ?? '', + deskripsi: data.deskripsi ?? '', + link: data.link ?? '', + }); + setOriginalData({ + name: data.name ?? '', + deskripsi: data.deskripsi ?? '', + link: data.link ?? '', }); } } catch (error) { @@ -49,11 +61,31 @@ function EditPelayananTelunjukSakti() { toast.error('Gagal memuat data pelayanan telunjuk sakti'); } }; - loadPelayananTelunjukSakti(); + + loadData(); }, [params?.id]); + // Handler input controlled + const handleChange = useCallback( + (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }, + [] + ); + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + link: originalData.link, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + // Submit: update global state hanya saat simpan const handleSubmit = async () => { try { + setIsSubmitting(true); stateTelunjukDesa.edit.form = { ...stateTelunjukDesa.edit.form, ...formData, @@ -64,6 +96,8 @@ function EditPelayananTelunjukSakti() { } catch (error) { console.error('Error updating pelayanan telunjuk sakti:', error); toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti'); + } finally { + setIsSubmitting(false); } }; @@ -71,11 +105,9 @@ function EditPelayananTelunjukSakti() { {/* Back Button + Title */} - - Edit Pelayanan Telunjuk Sakti Desa @@ -95,31 +127,40 @@ function EditPelayananTelunjukSakti() { label="Nama Pelayanan" placeholder="Masukkan nama pelayanan" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange('name', e.target.value)} required /> - {/* Deskripsi pakai editor */} - - - Deskripsi - - setFormData({ ...formData, deskripsi: htmlContent })} - /> - + {/* Deskripsi */} + handleChange('deskripsi', e.target.value)} + label="Judul Link" + placeholder="Masukkan judul link" + required + /> {/* Link */} setFormData({ ...formData, link: e.target.value })} + onChange={(e) => handleChange('link', e.target.value)} /> {/* Tombol Simpan */} + + + {/* 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..180534c2 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx @@ -9,8 +9,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -103,6 +102,7 @@ function DetailPelayananTelunjukSakti() { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', + wordBreak: "break-word", }} > {data.link} @@ -124,11 +124,11 @@ function DetailPelayananTelunjukSakti() { dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-', }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> - - - - diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx index 5cf7028f..28faa0ff 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx @@ -6,21 +6,22 @@ import { Box, Button, Group, + Loader, Paper, Stack, - Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; +import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreatePelayananTelunjukDesa() { const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateTelunjukDesa.create.form = { @@ -32,6 +33,7 @@ function CreatePelayananTelunjukDesa() { const handleSubmit = async () => { try { + setIsSubmitting(true); await stateTelunjukDesa.create.create(); resetForm(); toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan'); @@ -39,6 +41,8 @@ function CreatePelayananTelunjukDesa() { } catch (error) { console.error('Error create pelayanan telunjuk sakti:', error); toast.error('Terjadi kesalahan saat menambahkan data'); + } finally { + setIsSubmitting(false); } }; @@ -46,11 +50,9 @@ function CreatePelayananTelunjukDesa() { {/* Header */} - - Tambah Pelayanan Telunjuk Sakti Desa @@ -72,7 +74,7 @@ function CreatePelayananTelunjukDesa() { onChange={(val) => { stateTelunjukDesa.create.form.name = val.target.value; }} - label={Nama Pelayanan} + label="Nama Pelayanan" placeholder="Masukkan nama pelayanan telunjuk sakti desa" required /> @@ -83,8 +85,9 @@ function CreatePelayananTelunjukDesa() { onChange={(val) => { stateTelunjukDesa.create.form.deskripsi = val.target.value; }} - label={Deskripsi} - placeholder="Masukkan deskripsi pelayanan" + label="Judul Link" + placeholder="Masukkan judul link" + required /> {/* Link */} @@ -93,12 +96,24 @@ function CreatePelayananTelunjukDesa() { onChange={(val) => { stateTelunjukDesa.create.form.link = val.target.value; }} - label={Link} + label="Link" placeholder="Masukkan link pelayanan" + required /> {/* Tombol Simpan */} + + + {/* 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..b553773f 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx @@ -1,159 +1,3 @@ -// /* eslint-disable react-hooks/exhaustive-deps */ -// 'use client' -// import colors from '@/con/colors'; -// import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; -// import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; -// import { useRouter } from 'next/navigation'; -// import { useEffect, useMemo, useState } from 'react'; -// import { useProxy } from 'valtio/utils'; -// import HeaderSearch from '../../../_com/header'; -// import JudulList from '../../../_com/judulList'; -// import stateLayananDesa from '../../../_state/desa/layananDesa'; - -// function PelayananTelunjukSakti() { -// const [search, setSearch] = useState(""); -// return ( -// -// } -// value={search} -// onChange={(e) => setSearch(e.currentTarget.value)} -// /> -// -// -// ); -// } - -// function ListPelayananTelunjukSakti({ search }: { search: string }) { -// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) -// const router = useRouter() - -// const { -// data, -// page, -// totalPages, -// loading, -// load, -// } = telunjukSaktiState.findMany; - -// useEffect(() => { -// load(page, 10) -// }, []) - -// const filteredData = useMemo(() => { -// if (!data) return []; -// return data.filter(item => { -// const keyword = search.toLowerCase(); -// return ( -// item.name?.toLowerCase().includes(keyword) || -// item.link?.toLowerCase().includes(keyword) || -// item.deskripsi?.toLowerCase().includes(keyword) -// ); -// }) -// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki); -// }, [data, search]); - -// if (loading || !data) { -// return ( -// -// -// -// ); -// } - -// if (data.length === 0) { -// return ( -// -// -// -//
-// -// -// Nama -// Link -// Detail -// -// -// -// -// -// -// Tidak ada data -// -// -// -// -//
-//
-//
-// ); -// } - -// return ( -// -// -// -// -// -// -// Nama -// Link -// Detail -// -// -// -// {filteredData.map((item) => ( -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// ))} -// -//
-//
-//
-// { -// load(newPage, 10); -// window.scrollTo(0, 0); -// }} -// total={totalPages} -// mt="md" -// mb="md" -// /> -//
-//
-// ); -// } - -// export default PelayananTelunjukSakti; - /* eslint-disable react-hooks/exhaustive-deps */ 'use client' @@ -174,8 +18,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -225,7 +68,6 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) { Daftar Pelayanan Telunjuk Sakti - - @@ -261,7 +102,7 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) { - + diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx index 8e5606dc..53ec0c12 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx @@ -5,16 +5,17 @@ import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -24,18 +25,31 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function EditPenghargaan() { - const statePenghargaan = useProxy(penghargaanState) - const router = useRouter() - const params = useParams() - const [previewImage, setPreviewImage] = useState(null) - const [file, setFile] = useState(null) - const [formData, setFormData] = useState({ - name: statePenghargaan.findUnique.data?.name || '', - juara: statePenghargaan.findUnique.data?.juara || '', - deskripsi: statePenghargaan.findUnique.data?.deskripsi || '', - imageId: statePenghargaan.findUnique.data?.imageId || '', - }) + const statePenghargaan = useProxy(penghargaanState); + const router = useRouter(); + const params = useParams(); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + juara: "", + deskripsi: "", + imageId: "", + imageUrl: "", + }); + + // Lokal formData + const [formData, setFormData] = useState({ + name: '', + juara: '', + deskripsi: '', + imageId: '', + }); + + // Load data pertama kali useEffect(() => { const loadPenghargaan = async () => { const id = params?.id as string; @@ -43,66 +57,88 @@ function EditPenghargaan() { try { const data = await statePenghargaan.edit.load(id); + if (data) { - setFormData({ - name: data.name || '', - juara: data.juara || '', - deskripsi: data.deskripsi || '', - imageId: data.imageId || '', + const newForm = { + name: data.name || "", + juara: data.juara || "", + deskripsi: data.deskripsi || "", + imageId: data.imageId || "", + }; + setFormData(newForm); + + // simpan juga versi original + const imageUrl = data.image?.link || ""; + + setOriginalData({ + ...newForm, + imageUrl: imageUrl, }); - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + setPreviewImage(imageUrl || null); } } catch (error) { - console.error("Error loading penghargaan:", error); - toast.error("Gagal memuat data penghargaan"); + console.error('Error loading penghargaan:', error); + toast.error('Gagal memuat data penghargaan'); } }; loadPenghargaan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + juara: originalData.juara, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + // Submit const handleSubmit = async () => { try { + setIsSubmitting(true); + // Sync ke global state saat submit + let imageId = formData.imageId; + if (file) { + const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error("Gagal upload gambar"); + } + imageId = uploaded.id; + } + + // Update global state form (baru di sini) statePenghargaan.edit.form = { ...statePenghargaan.edit.form, name: formData.name, juara: formData.juara, deskripsi: formData.deskripsi, - imageId: formData.imageId, + imageId, } - - if (file) { - const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } - - statePenghargaan.edit.form.imageId = uploaded.id; - } - await statePenghargaan.edit.update(); - toast.success("Penghargaan berhasil diperbarui!"); - router.push("/admin/desa/penghargaan"); + toast.success('Penghargaan berhasil diperbarui!'); + router.push('/admin/desa/penghargaan'); } catch (error) { - console.error("Error updating penghargaan:", error); - toast.error("Terjadi kesalahan saat memperbarui penghargaan"); + console.error('Error updating penghargaan:', error); + toast.error('Terjadi kesalahan saat memperbarui penghargaan'); + } finally { + setIsSubmitting(false); } - } + }; return ( {/* Tombol Back + Title */} - - - + Edit Penghargaan @@ -123,7 +159,7 @@ function EditPenghargaan() { label="Judul" placeholder="Masukkan judul penghargaan" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))} required /> @@ -132,7 +168,7 @@ function EditPenghargaan() { label="Juara" placeholder="Masukkan juara" value={formData.juara} - onChange={(e) => setFormData({ ...formData, juara: e.target.value })} + onChange={(e) => setFormData((prev) => ({ ...prev, juara: e.target.value }))} required /> @@ -151,7 +187,7 @@ function EditPenghargaan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -170,21 +206,47 @@ function EditPenghargaan() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - - + + + + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -196,15 +258,25 @@ function EditPenghargaan() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - statePenghargaan.edit.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx index a8d902e4..fb8fb55e 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx @@ -1,9 +1,5 @@ 'use client' -import React, { useState } from 'react'; -import penghargaanState from '../../../_state/desa/penghargaan'; -import { useProxy } from 'valtio/utils'; -import { useParams, useRouter } from 'next/navigation'; -import { useShallowEffect } from '@mantine/hooks'; +import colors from '@/con/colors'; import { Box, Button, @@ -12,12 +8,15 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; -import colors from '@/con/colors'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import penghargaanState from '../../../_state/desa/penghargaan'; function DetailPenghargaan() { const statePenghargaan = useProxy(penghargaanState); @@ -122,38 +121,35 @@ function DetailPenghargaan() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> - - - + - - - + diff --git a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx index 94a3c4e4..a76063c9 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -27,6 +28,7 @@ function CreatePenghargaan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { statePenghargaan.create.form = { @@ -40,37 +42,43 @@ function CreatePenghargaan() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + statePenghargaan.create.form.imageId = uploaded.id; + + await statePenghargaan.create.create(); + resetForm(); + router.push('/admin/desa/penghargaan'); + } catch (error) { + console.error('Error creating penghargaan:', error); + toast.error('Terjadi kesalahan saat menambahkan penghargaan'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - statePenghargaan.create.form.imageId = uploaded.id; - - await statePenghargaan.create.create(); - resetForm(); - router.push('/admin/desa/penghargaan'); }; return ( {/* Header */} - - - + Tambah Penghargaan @@ -89,7 +97,7 @@ function CreatePenghargaan() { (statePenghargaan.create.form.name = val.target.value)} - label={Nama Penghargaan} + label="Nama Penghargaan" placeholder="Masukkan nama penghargaan" required /> @@ -97,7 +105,7 @@ function CreatePenghargaan() { (statePenghargaan.create.form.juara = val.target.value)} - label={Juara} + label="Juara" placeholder="Masukkan juara" required /> @@ -125,7 +133,7 @@ function CreatePenghargaan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -146,7 +154,7 @@ function CreatePenghargaan() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Button Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx index 4e54e837..40e2420b 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx @@ -18,8 +18,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -69,7 +68,6 @@ function ListPenghargaan({ search }: { search: string }) { List Penghargaan - -
@@ -108,6 +105,7 @@ function ListPenghargaan({ search }: { search: string }) { fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> diff --git a/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx index e8702d32..3ae568ed 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { IconCategory, IconListDetails } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconListDetails, IconCategory } from '@tabler/icons-react'; function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { const router = useRouter() @@ -14,15 +14,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { label: "List Pengumuman", value: "listpengumuman", href: "/admin/desa/pengumuman/list-pengumuman", - icon: , - tooltip: "Lihat semua daftar pengumuman" + icon: }, { label: "Kategori Pengumuman", value: "kategoripengumuman", href: "/admin/desa/pengumuman/kategori-pengumuman", - icon: , - tooltip: "Kelola kategori pengumuman" + icon: }, ]; @@ -70,19 +68,18 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx index fe07bfd4..1438347d 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; + import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import colors from '@/con/colors'; import { @@ -10,8 +11,7 @@ import { Stack, TextInput, Title, - Tooltip, - Text, + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -23,11 +23,11 @@ function EditKategoriPengumuman() { const editState = useProxy(stateDesaPengumuman.category); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ name: '' }); + const [originalData, setOriginalData] = useState({ name: '' }); - const [formData, setFormData] = useState({ - name: editState.update.form.name || '', - }); - + // Load data awal sekali aja useEffect(() => { const loadKategori = async () => { const id = params?.id as string; @@ -36,9 +36,8 @@ function EditKategoriPengumuman() { try { const data = await editState.update.load(id); if (data) { - setFormData({ - name: data.name || '', - }); + setFormData({ name: data.name || '' }); + setOriginalData({ name: data.name || '' }); } } catch (error) { console.error('Error loading kategori Pengumuman:', error); @@ -49,8 +48,17 @@ function EditKategoriPengumuman() { loadKategori(); }, [params?.id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); + // Update global state hanya di sini editState.update.form = { ...editState.update.form, name: formData.name, @@ -62,14 +70,23 @@ function EditKategoriPengumuman() { } catch (error) { console.error('Error updating kategori Pengumuman:', error); toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + return ( {/* Header */} - - Edit Kategori Pengumuman @@ -95,20 +111,25 @@ function EditKategoriPengumuman() { > - Nama Kategori Pengumuman - - } + label="Nama Kategori Pengumuman" placeholder="Masukkan nama kategori Pengumuman" value={formData.name} - onChange={(e) => - setFormData({ ...formData, name: e.target.value }) - } + onChange={(e) => handleChange('name', e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx index 1284d18f..da1fb9c4 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx @@ -7,18 +7,20 @@ import { Group, Paper, Stack, - Text, TextInput, Title, - Tooltip, + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; function CreateKategoriPengumuman() { const createState = useProxy(stateDesaPengumuman.category); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { createState.create.form = { @@ -27,16 +29,22 @@ function CreateKategoriPengumuman() { }; const handleSubmit = async () => { - await createState.create.create(); - resetForm(); - router.push('/admin/desa/pengumuman/kategori-pengumuman'); + try { + await createState.create.create(); + resetForm(); + router.push('/admin/desa/pengumuman/kategori-pengumuman'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan kategori pengumuman'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan back button */} - - Tambah Kategori Pengumuman @@ -62,7 +69,7 @@ function CreateKategoriPengumuman() { > Nama Kategori Pengumuman} + label="Nama Kategori Pengumuman" placeholder="Masukkan nama kategori pengumuman" value={createState.create.form.name || ''} onChange={(e) => (createState.create.form.name = e.target.value)} @@ -70,6 +77,17 @@ function CreateKategoriPengumuman() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx index 541c0cae..bbd20857 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx @@ -2,11 +2,13 @@ 'use client' import colors from '@/con/colors'; import { - Box, Button, Center, Paper, Skeleton, Stack, + Box, Button, Center, + Pagination, + Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, - Text, Title, Tooltip, Pagination + Text, Title } from '@mantine/core'; -import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -66,16 +68,14 @@ function ListKategoriPengumuman({ search }: { search: string }) { List Kategori Pengumuman - - - + @@ -99,29 +99,25 @@ function ListKategoriPengumuman({ search }: { search: string }) { {item.name} - - - + - - - + )) diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx index 745672ee..cbeeceef 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx @@ -14,7 +14,7 @@ import { Text, TextInput, Title, - Tooltip, + Loader } from "@mantine/core"; import { IconArrowBack } from "@tabler/icons-react"; import { useParams, useRouter } from "next/navigation"; @@ -28,16 +28,25 @@ function EditPengumuman() { const params = useParams(); const [formData, setFormData] = useState({ - judul: editState.pengumuman.edit.form.judul || "", - deskripsi: editState.pengumuman.edit.form.deskripsi || "", - categoryPengumumanId: - editState.pengumuman.edit.form.categoryPengumumanId || "", - content: editState.pengumuman.edit.form.content || "", + judul: "", + deskripsi: "", + categoryPengumumanId: "", + content: "", }); - // Load pengumuman by id saat pertama kali + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + judul: "", + deskripsi: "", + categoryPengumumanId: "", + content: "", + }); + + // Load kategori & pengumuman by id saat pertama kali useEffect(() => { editState.category.findMany.load(); + const loadpengumuman = async () => { const id = params?.id as string; if (!id) return; @@ -51,6 +60,12 @@ function EditPengumuman() { categoryPengumumanId: data.categoryPengumumanId || "", content: data.content || "", }); + setOriginalData({ + judul: data.judul || "", + deskripsi: data.deskripsi || "", + categoryPengumumanId: data.categoryPengumumanId || "", + content: data.content || "", + }); } } catch (error) { console.error("Error loading pengumuman:", error); @@ -61,9 +76,14 @@ function EditPengumuman() { loadpengumuman(); }, [params?.id]); + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { try { - // update global state + setIsSubmitting(true); + // update global state hanya sekali pas submit editState.pengumuman.edit.form = { ...editState.pengumuman.edit.form, ...formData, @@ -75,22 +95,32 @@ function EditPengumuman() { } catch (error) { console.error("Error updating pengumuman:", error); toast.error("Terjadi kesalahan saat memperbarui pengumuman"); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + categoryPengumumanId: originalData.categoryPengumumanId, + content: originalData.content, + }); + toast.info("Form dikembalikan ke data awal"); + }; + return ( - - - + Edit Pengumuman @@ -109,7 +139,7 @@ function EditPengumuman() { label="Judul Pengumuman" placeholder="Masukkan judul" value={formData.judul} - onChange={(e) => setFormData({ ...formData, judul: e.target.value })} + onChange={(e) => handleChange("judul", e.target.value)} required /> @@ -117,17 +147,13 @@ function EditPengumuman() { label="Deskripsi Singkat" placeholder="Masukkan deskripsi" value={formData.deskripsi} - onChange={(e) => - setFormData({ ...formData, deskripsi: e.target.value }) - } + onChange={(e) => handleChange("deskripsi", e.target.value)} required /> Kategori} + label="Kategori" placeholder="Pilih kategori" - value={pengumumanState.pengumuman.create.form.categoryPengumumanId || ""} - onChange={(val) => { - pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? ""; - }} data={pengumumanState.category.findMany.data?.map((item) => ({ label: item.name, value: item.id, - }))} + })) || []} + value={pengumumanState.pengumuman.create.form.categoryPengumumanId || null} + onChange={(val: string | null) => { + if (val) { + const selected = pengumumanState.category.findMany.data?.find( + (item) => item.id === val + ); + if (selected) { + pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id; + } + } else { + pengumumanState.pengumuman.create.form.categoryPengumumanId = ''; + } + }} searchable + clearable nothingFoundMessage="Tidak ditemukan" + required /> {/* Deskripsi Singkat */} (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)} - label={Deskripsi Singkat} + label="Deskripsi Singkat" placeholder="Masukkan deskripsi singkat" required /> @@ -115,6 +135,17 @@ function CreatePengumuman() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx index e4c819e4..358fc1c5 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx @@ -17,8 +17,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; @@ -69,16 +68,14 @@ function ListPengumuman({ search }: { search: string }) { Daftar Pengumuman - - - +
diff --git a/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx index dc981aff..cccd64a8 100644 --- a/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { IconCategory, IconListCheck } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; @@ -14,15 +14,13 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) { label: "List Potensi", value: "list_potensi", href: "/admin/desa/potensi/list-potensi", - icon: , - tooltip: "Lihat semua potensi desa" + icon: }, { label: "Kategori Potensi", value: "kategori_potensi", href: "/admin/desa/potensi/kategori-potensi", - icon: , - tooltip: "Kelola kategori potensi" + icon: }, ]; @@ -70,19 +68,18 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx index 73092b8f..4c9f4c26 100644 --- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx @@ -10,7 +10,7 @@ import { Stack, TextInput, Title, - Tooltip + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -24,9 +24,16 @@ function EditKategoriPotensi() { const params = useParams(); const [formData, setFormData] = useState({ - nama: editState.update.form.nama || '', + nama: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + + // Load data dari backend -> isi ke formData lokal useEffect(() => { const loadKategori = async () => { const id = params?.id as string; @@ -38,6 +45,9 @@ function EditKategoriPotensi() { setFormData({ nama: data.nama || '', }); + setOriginalData({ + nama: data.nama || '', + }); } } catch (error) { console.error('Error loading kategori potensi:', error); @@ -48,8 +58,24 @@ function EditKategoriPotensi() { loadKategori(); }, [params?.id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); + // Update global state hanya pas submit editState.update.form = { ...editState.update.form, nama: formData.nama, @@ -61,17 +87,22 @@ function EditKategoriPotensi() { } catch (error) { console.error('Error updating kategori potensi:', error); toast.error('Terjadi kesalahan saat memperbarui kategori potensi'); + } finally { + setIsSubmitting(false); } }; return ( - - - Edit Kategori Potensi @@ -90,11 +121,22 @@ function EditKategoriPotensi() { label="Nama Kategori Potensi" placeholder="Masukkan nama kategori potensi" value={formData.nama} - onChange={(e) => setFormData({ ...formData, nama: e.target.value })} + onChange={(e) => handleChange('nama', e.currentTarget.value)} required /> - + + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx index 61a46a0a..5647f386 100644 --- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx @@ -7,18 +7,19 @@ import { Group, Paper, Stack, - Text, TextInput, Title, - Tooltip, + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; function CreateKategoriPotensi() { const createState = useProxy(potensiDesaState.kategoriPotensi); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { createState.create.form = { @@ -27,25 +28,30 @@ function CreateKategoriPotensi() { }; const handleSubmit = async () => { - await createState.create.create(); - resetForm(); - router.push('/admin/desa/potensi/kategori-potensi'); + try { + setIsSubmitting(true); + await createState.create.create(); + resetForm(); + router.push('/admin/desa/potensi/kategori-potensi'); + } catch (error) { + console.error('Error creating kategori potensi:', error); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan back button */} - - - + Tambah Kategori Potensi @@ -62,7 +68,7 @@ function CreateKategoriPotensi() { > Nama Kategori Potensi} + label="Nama Kategori Potensi" placeholder="Masukkan nama kategori potensi" value={createState.create.form.nama || ''} onChange={(e) => (createState.create.form.nama = e.target.value)} @@ -70,6 +76,17 @@ function CreateKategoriPotensi() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx index 3d408f8f..55e51247 100644 --- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx @@ -1,14 +1,14 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination, Group } from '@mantine/core'; -import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; -import potensiDesaState from '../../../_state/desa/potensi'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import potensiDesaState from '../../../_state/desa/potensi'; function KategoriPotensi() { const [search, setSearch] = useState(''); @@ -62,7 +62,6 @@ function ListKategoriPotensi({ search }: { search: string }) { List Kategori Potensi - - diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx index 1bcdc41a..bf2197e6 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx @@ -16,7 +16,8 @@ import { Text, TextInput, Title, - Tooltip, + Loader, + ActionIcon } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; @@ -39,9 +40,25 @@ function EditPotensi() { content: "", imageId: "", }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + kategoriId: "", + content: "", + imageId: "", + imageUrl: "", + }); + + // handle input changes + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; useEffect(() => { potensiDesaState.kategoriPotensi.findMany.load(); + const loadPotensi = async () => { const id = params?.id as string; if (!id) return; @@ -57,9 +74,16 @@ function EditPotensi() { imageId: data.imageId || "", }); - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + setOriginalData({ + name: data.name || "", + deskripsi: data.deskripsi || "", + kategoriId: data.kategoriId || "", + content: data.content || "", + imageId: data.imageId || "", + imageUrl: data.image?.link || "", + }); + setPreviewImage(data.image.link); + } } catch (error) { console.error("Error loading potensi:", error); @@ -70,41 +94,65 @@ function EditPotensi() { loadPotensi(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name || "", + deskripsi: originalData.deskripsi || "", + kategoriId: originalData.kategoriId || "", + content: originalData.content || "", + imageId: originalData.imageId || "" + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { - potensiState.edit.form = { - ...potensiState.edit.form, - ...formData, - }; - + setIsSubmitting(true); + let imageId = formData.imageId; if (file) { - const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); const uploaded = res.data?.data; if (!uploaded?.id) { return toast.error("Gagal upload gambar"); } - potensiState.edit.form.imageId = uploaded.id; + imageId = uploaded.id; } + potensiState.edit.form = { + ...formData, + imageId, + }; + await potensiState.edit.update(); toast.success("Potensi berhasil diperbarui!"); router.push("/admin/desa/potensi/list-potensi"); } catch (error) { console.error("Error updating potensi:", error); toast.error("Terjadi kesalahan saat memperbarui potensi"); + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Potensi Desa @@ -123,21 +171,51 @@ function EditPotensi() { label="Judul Potensi" placeholder="Masukkan judul" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange("name", e.target.value)} required /> - setFormData({ ...formData, deskripsi: e.target.value })} - required - /> + + + Deskripsi Singkat + + + handleChange("deskripsi", htmlContent) + } + /> + setFormData({ ...formData, kategoriId: val || "" })} + onChange={(val) => handleChange("kategoriId", val || "")} label="Kategori" placeholder="Pilih kategori" data={ @@ -150,7 +228,7 @@ function EditPotensi() { searchable required error={!formData.kategoriId ? "Pilih kategori" : undefined} - /> + /> */} @@ -164,7 +242,9 @@ function EditPotensi() { setPreviewImage(URL.createObjectURL(selectedFile)); } }} - onReject={() => toast.error("File tidak valid, gunakan format gambar")} + onReject={() => + toast.error("File tidak valid, gunakan format gambar") + } maxSize={5 * 1024 ** 2} accept={{ "image/*": [] }} radius="md" @@ -172,7 +252,11 @@ function EditPotensi() { > - + @@ -185,25 +269,45 @@ function EditPotensi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -214,22 +318,36 @@ function EditPotensi() { setFormData({ ...formData, content: htmlContent })} + onChange={(htmlContent) => + handleChange("content", htmlContent) + } /> + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx index dc40fae6..b2e5cf17 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx @@ -1,13 +1,13 @@ 'use client' -import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; -import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; -import { useRouter, useParams } from 'next/navigation'; -import { useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; +import colors from '@/con/colors'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; export default function DetailPotensi() { const router = useRouter(); @@ -25,7 +25,7 @@ export default function DetailPotensi() { potensiState.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - router.push("/admin/desa/potensi"); + router.push("/admin/desa/potensi/list-potensi"); } }; @@ -77,7 +77,7 @@ export default function DetailPotensi() { Deskripsi - {data.deskripsi || '-'} + @@ -103,11 +103,11 @@ export default function DetailPotensi() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.content || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> - - - - diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx index b7a9f1dd..d18bca1b 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx @@ -15,7 +15,8 @@ import { Text, TextInput, Title, - Tooltip, + Loader, + ActionIcon } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -29,30 +30,39 @@ function CreatePotensi() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { potensiDesaState.kategoriPotensi.findMany.load(); }, []); const handleSubmit = async () => { - if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + potensiState.create.form.imageId = uploaded.id; + + await potensiState.create.create(); + + resetForm(); + router.push('/admin/desa/potensi/list-potensi'); + } catch (error) { + console.error('Error creating potensi:', error); + toast.error('Terjadi kesalahan saat menambahkan potensi'); + } finally { + setIsSubmitting(false); } - - potensiState.create.form.imageId = uploaded.id; - - await potensiState.create.create(); - - resetForm(); - router.push('/admin/desa/potensi/list-potensi'); }; const resetForm = () => { @@ -72,11 +82,9 @@ function CreatePotensi() { {/* Header */} - - - + Tambah Potensi Desa @@ -95,23 +103,53 @@ function CreatePotensi() { (potensiState.create.form.name = val.target.value)} - label={Judul} + label="Judul" placeholder="Masukkan judul potensi" required /> {/* Deskripsi */} - (potensiState.create.form.deskripsi = val.target.value)} - label={Deskripsi} - placeholder="Masukkan deskripsi singkat" - required - /> + + + Deskripsi Singkat + + { + potensiState.create.form.deskripsi = htmlContent; + }} + /> + {/* Kategori */} { @@ -121,7 +159,7 @@ function CreatePotensi() { value: item.id, label: item.nama, }))} - /> + /> */} {/* Upload Gambar */} @@ -138,7 +176,7 @@ function CreatePotensi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -156,17 +194,44 @@ function CreatePotensi() { Seret gambar atau klik untuk memilih file (maks 5MB) + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -186,6 +251,17 @@ function CreatePotensi() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx index 2a7c6eb8..9ee2259b 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx @@ -18,8 +18,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -76,7 +75,6 @@ function ListPotensi({ search }: { search: string }) { Daftar Potensi Desa - -
@@ -102,19 +99,25 @@ function ListPotensi({ search }: { search: string }) { filteredData.map((item) => ( - - {item.name} - + + + {item.name} + + - {item.kategori?.nama || '-'} + + {item.kategori?.nama || '-'} + diff --git a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx index 7554a1c8..25771ec3 100644 --- a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx +++ b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { IconCalendar, IconUser, IconUsers } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react'; function LayoutTabsDetail({ children }: { children: React.ReactNode }) { const router = useRouter() @@ -14,22 +14,19 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) { label: "Profile Desa", value: "profiledesa", href: "/admin/desa/profile/profile-desa", - icon: , - tooltip: "Lihat dan kelola profil desa" + icon: }, { label: "Profile Perbekel", value: "profileperbekel", href: "/admin/desa/profile/profile-perbekel", - icon: , - tooltip: "Kelola data Perbekel" + icon: }, { label: "Profile Perbekel Dari Masa Ke Masa", value: "profile-perbekel-dari-masa-ke-masa", href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa", - icon: , - tooltip: "Riwayat Perbekel dari masa ke masa" + icon: } ]; @@ -76,42 +73,41 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) { paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi }} > - {tabs.map((tab, i) => ( - - - {tab.label} - - - ))} - - - {tabs.map((tab, i) => ( - - {/* Konten dummy, bisa diganti sesuai routing */} - <>{children} - + {tab.label} + ))} - - - ); + + + + {tabs.map((tab, i) => ( + + {/* Konten dummy, bisa diganti sesuai routing */} + <>{children} + + ))} + + + ); } - export default LayoutTabsDetail; +export default LayoutTabsDetail; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx index c0ee0456..6d182103 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx @@ -1,154 +1,244 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title, +} from '@mantine/core'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; +// 🧩 Type untuk form +interface FormData { + judul: string; + deskripsi: string; +} + +// 🧩 Main Component function Page() { - const lambangState = useProxy(stateProfileDesa.lambangDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); - // Load data - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); - return; - } + const router = useRouter(); + const params = useParams(); - try { - const data = await lambangState.findUnique.load(id); - lambangState.update.initialize(data); - } catch (error) { - console.error("Error loading lambang:", error); - toast.error("Gagal memuat data lambang desa"); - } - }; + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); - loadData(); + // 🧭 Load data awal + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/desa/profile/profile-desa'); + return; + } - return () => { - lambangState.update.reset(); - lambangState.findUnique.reset(); - }; - }, [params?.id, router]); + setIsLoading(true); + setLoadError(null); - const handleSubmit = async () => { - if (isSubmitting || !lambangState.update.form.judul.trim()) { - toast.error("Judul wajib diisi"); - return; - } - - setIsSubmitting(true); - - try { - const success = await lambangState.update.submit(); - - if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); - } - } catch (error) { - console.error("Error update lambang desa:", error); - toast.error("Terjadi kesalahan saat update lambang desa"); - } finally { - setIsSubmitting(false); + try { + const data = await stateProfileDesa.lambangDesa.findUnique.load(id); + + if (data) { + const initial: FormData = { + judul: data.judul || '', + deskripsi: data.deskripsi || '', + }; + setFormData(initial); + setOriginalData(initial); + + // Penting untuk isi id di state sebelum submit + stateProfileDesa.lambangDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); } + } catch (error) { + console.error('Error loading lambang:', error); + setLoadError('Gagal memuat data lambang desa'); + toast.error('Gagal memuat data lambang desa'); + } finally { + setIsLoading(false); + } }; - const handleBack = () => router.back(); + loadData(); - // Loading state - if (lambangState.findUnique.loading || lambangState.update.loading) { - return ( - -
- Memuat data... -
-
- ); + return () => { + stateProfileDesa.lambangDesa.update.reset(); + stateProfileDesa.lambangDesa.findUnique.reset(); + }; + }, [params?.id, router]); + + // 🔁 Reset form + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit handler + const handleSubmit = async () => { + if (!formData.judul.trim()) { + toast.error('Judul wajib diisi'); + return; } - // Error state - if (lambangState.findUnique.error) { - return ( - - - - } color="red"> - Error - {lambangState.findUnique.error} - - - - ); - } + setIsSubmitting(true); + try { + const state = stateProfileDesa.lambangDesa; + state.update.form.judul = formData.judul; + state.update.form.deskripsi = formData.deskripsi; + const success = await state.update.submit(); + + if (success) { + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profile/profile-desa'); + } else { + toast.error('Gagal menyimpan data'); + } + } catch (error) { + console.error('Error update lambang desa:', error); + toast.error('Terjadi kesalahan saat update lambang desa'); + } finally { + setIsSubmitting(false); + } + }; + + // 📝 Handlers + const handleJudulChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ ...prev, judul: e.target.value })); + }; + + const handleDeskripsiChange = (html: string) => { + setFormData(prev => ({ ...prev, deskripsi: html })); + }; + + const handleBack = () => router.back(); + + // 🔄 Loading + if (isLoading) { return ( - - - - - - - Edit Lambang Desa - - - - - Edit Lambang Desa - - {/* Judul */} - Judul} - placeholder="Judul lambang" - value={lambangState.update.form.judul} - onChange={(e) => lambangState.update.form.judul = e.currentTarget.value} - error={!lambangState.update.form.judul && "Judul wajib diisi"} - /> - - {/* Deskripsi */} - - Deskripsi - lambangState.update.form.deskripsi = val} - /> - - - {/* Buttons */} - - - - - - - - - + +
+ + + + Memuat data lambang desa... + + +
+
); + } + + // ❌ Error + if (loadError) { + return ( + + + + } color="red" title="Terjadi Kesalahan" radius="md"> + {loadError} + + + + + ); + } + + // 🧱 UI utama + return ( + + + {/* Header */} + + + + Edit Lambang Desa + + + + {/* Form */} + + + {/* Judul */} + Judul} + placeholder="Masukkan judul lambang desa" + value={formData.judul} + onChange={handleJudulChange} + error={!formData.judul.trim() && 'Judul wajib diisi'} + required + size="md" + radius="md" + /> + + {/* Deskripsi */} + + + Deskripsi + + + + + {/* Tombol Aksi */} + + + + + + + + + ); } export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx index 8926e3bf..a33601da 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx @@ -5,9 +5,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title, Tooltip, Center, Alert } from '@mantine/core'; +import { Alert, Box, Button, Center, Group, Image, Loader, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconPhoto, IconUpload, IconX, IconAlertCircle } from '@tabler/icons-react'; +import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; @@ -18,7 +18,9 @@ function Page() { const router = useRouter(); const params = useParams(); - const [images, setImages] = useState>([]); + const [images, setImages] = useState< + Array<{ file: File | null; preview: string; label: string; imageId?: string }> + >([]); const [formData, setFormData] = useState({ judul: '', deskripsi: '', @@ -26,6 +28,12 @@ function Page() { }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: "", + deskripsi: "", + images: [] as Array<{ label: string; imageId: string }> + }); + // Load data useEffect(() => { const loadData = async () => { @@ -50,11 +58,23 @@ function Page() { })), }); + setOriginalData({ + judul: data.judul || '', + deskripsi: data.deskripsi || '', + images: (data.images || []).map((img: any) => ({ + label: img.label, + imageId: img.image?.id ?? '', + preview: img.image?.link ?? '', + })), + }); + + if (data?.images?.length > 0 && data.images[0].image?.link) { setImages(data.images.map((img: any) => ({ file: null, preview: img.image.link, label: img.label, + imageId: img.image.id, // simpan id lama }))); } } @@ -74,40 +94,60 @@ function Page() { const handleBack = () => router.back(); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + images: originalData.images.map((img) => ({ + label: img.label, + imageId: img.imageId, + })), + }); + + setImages( + originalData.images.map((img: any) => ({ + file: null, + preview: img.preview, // pakai preview masing-masing, bukan cuma satu + label: img.label, + imageId: img.imageId, + })) + ); + + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { if (isSubmitting || !formData.judul.trim()) { toast.error("Judul wajib diisi"); return; } - - setIsSubmitting(true); - try { + setIsSubmitting(true); const uploadedImages = []; // Upload semua gambar baru for (const img of images) { if (!img.file) { - // Kalau gambar lama, skip upload - if (!img.preview) continue; - uploadedImages.push({ imageId: '', label: img.label }); + if (!img.imageId) continue; // kalau benar2 kosong, skip + uploadedImages.push({ imageId: img.imageId, label: img.label }); continue; } + // upload baru const res = await ApiFetch.api.fileStorage.create.post({ file: img.file, name: img.file.name, }); - const uploaded = res.data?.data; if (!uploaded?.id) { toast.error("Gagal upload salah satu gambar"); return; } - - uploadedImages.push({ imageId: uploaded.id, label: img.label || 'main' }); + uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" }); } + // Update ke global state maskotState.update.updateField("judul", formData.judul); maskotState.update.updateField("deskripsi", formData.deskripsi); @@ -159,11 +199,9 @@ function Page() { - - - + Edit Maskot Desa @@ -231,7 +269,7 @@ function Page() { setImages(updated); }} > - Hapus + {/* Buttons */} - + + {/* Tombol Batal */} - + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx index 89bd1d64..7bbd5c16 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx @@ -1,148 +1,272 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title +} from '@mantine/core'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; +// 🔹 Types +interface FormData { + judul: string; + deskripsi: string; +} + +// 🔹 Main Component function Page() { - const sejarahState = useProxy(stateProfileDesa.sejarahDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); + const router = useRouter(); + const params = useParams(); - // Load data + // 🧩 Local State + const [formData, setFormData] = useState({ + judul: '', + deskripsi: '', + }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); + + // 🧭 Load Initial Data useEffect(() => { const loadData = async () => { const id = params?.id as string; if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); + toast.error('ID tidak valid'); + router.push('/admin/desa/profile/profile-desa'); return; } + setIsLoading(true); + setLoadError(null); + try { - const data = await sejarahState.findUnique.load(id); + const data = await stateProfileDesa.sejarahDesa.findUnique.load(id); + if (data) { - sejarahState.update.initialize(data); + const initialData: FormData = { + judul: data.judul || '', + deskripsi: data.deskripsi || '', + }; + + setFormData(initialData); + setOriginalData(initialData); + + stateProfileDesa.sejarahDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); } } catch (error) { - console.error("Error loading sejarah:", error); - toast.error("Gagal memuat data sejarah desa"); + console.error('Error loading sejarah:', error); + setLoadError('Gagal memuat data sejarah desa'); + toast.error('Gagal memuat data sejarah desa'); + } finally { + setIsLoading(false); } }; loadData(); return () => { - sejarahState.update.reset(); - sejarahState.findUnique.reset(); + stateProfileDesa.sejarahDesa.update.reset(); + stateProfileDesa.sejarahDesa.findUnique.reset(); }; }, [params?.id, router]); + // 🔄 Check if form has changes + + + // 🔁 Reset Form to Original Data + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit Handler const handleSubmit = async () => { - if (isSubmitting || !sejarahState.update.form.judul.trim()) { - toast.error("Judul wajib diisi"); + // Validation + if (!formData.judul.trim()) { + toast.error('Judul wajib diisi'); return; } setIsSubmitting(true); + try { - const success = await sejarahState.update.submit(); + // Access original state directly (not proxy) + const originalState = stateProfileDesa.sejarahDesa; + + // Update form data + originalState.update.form.judul = formData.judul; + originalState.update.form.deskripsi = formData.deskripsi; + + // Submit + const success = await originalState.update.submit(); + if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profile/profile-desa'); + } else { + toast.error('Gagal menyimpan data'); } } catch (error) { - console.error("Error update sejarah desa:", error); - toast.error("Terjadi kesalahan saat update sejarah desa"); + console.error('Error update sejarah desa:', error); + toast.error('Terjadi kesalahan saat update sejarah desa'); } finally { setIsSubmitting(false); } }; - const handleBack = () => router.back(); + // 📝 Form Field Handlers + const handleJudulChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ ...prev, judul: e.target.value })); + }; - // Loading state - if (sejarahState.findUnique.loading || sejarahState.update.loading) { + const handleDeskripsiChange = (html: string) => { + setFormData(prev => ({ ...prev, deskripsi: html })); + }; + + const handleBack = () => { + router.back(); + }; + // 🔄 Loading State + if (isLoading) { return (
- Memuat data... + + + + Memuat data... + +
); } - // Error state - if (sejarahState.findUnique.error) { + // ❌ Error State + if (loadError) { return ( - + - - } color="red"> - Error - {sejarahState.findUnique.error} + } + color="red" + title="Terjadi Kesalahan" + radius="md" + > + {loadError} + ); } return ( - - - - - - - Edit Sejarah Desa + + + {/* Header */} + + + + Edit Sejarah Desa + - - - Edit Sejarah Desa - - {/* Judul */} + {/* Form */} + + + {/* Form Title */} + + + Informasi Sejarah Desa + + + {/* Judul Field */} Judul} - placeholder="Judul sejarah" - value={sejarahState.update.form.judul} - onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value} - error={!sejarahState.update.form.judul && "Judul wajib diisi"} + label={Judul Sejarah} + placeholder="Masukkan judul sejarah desa" + value={formData.judul} + onChange={handleJudulChange} + error={!formData.judul.trim() && 'Judul wajib diisi'} + required + size="md" + radius="md" /> - {/* Deskripsi */} + {/* Deskripsi Field */} - Deskripsi + + Deskripsi Sejarah + sejarahState.update.form.deskripsi = val} + value={formData.deskripsi} + onChange={handleDeskripsiChange} /> + - {/* Buttons */} - + {/* Action Buttons */} + + {/* Tombol Batal */} - @@ -152,4 +276,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx index 0a06bc88..938172b1 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx @@ -1,155 +1,247 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title, Tooltip } from '@mantine/core'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + Title, +} from '@mantine/core'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; +// 🔹 Types +interface FormData { + visi: string; + misi: string; +} + +// 🔹 Main Component function Page() { - const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); + const router = useRouter(); + const params = useParams(); + const [formData, setFormData] = useState({ visi: '', misi: '' }); + const [originalData, setOriginalData] = useState({ visi: '', misi: '' }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); - // Load data - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); - return; - } + // 🧭 Load Data + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/desa/profile/profile-desa'); + return; + } - try { - const data = await visiMisiState.findUnique.load(id); - visiMisiState.update.initialize(data); - } catch (error) { - console.error("Error loading visi misi:", error); - toast.error("Gagal memuat data visi misi desa"); - } - }; + setIsLoading(true); + setLoadError(null); - loadData(); + try { + const data = await stateProfileDesa.visiMisiDesa.findUnique.load(id); + if (data) { + const initialData: FormData = { + visi: data.visi || '', + misi: data.misi || '', + }; + setFormData(initialData); + setOriginalData(initialData); - return () => { - visiMisiState.update.reset(); - visiMisiState.findUnique.reset(); - }; - }, [params?.id, router]); - - const handleSubmit = async () => { - if (isSubmitting || !visiMisiState.update.form.visi.trim()) { - toast.error("Visi wajib diisi"); - return; - } - - setIsSubmitting(true); - - try { - const success = await visiMisiState.update.submit(); - - if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); - } - } catch (error) { - console.error("Error update visi misi desa:", error); - toast.error("Terjadi kesalahan saat update visi misi desa"); - } finally { - setIsSubmitting(false); + // set id ke state agar submit pakai endpoint benar + stateProfileDesa.visiMisiDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); } + } catch (error) { + console.error('Error load visi misi:', error); + setLoadError('Gagal memuat data visi misi desa'); + toast.error('Gagal memuat data visi misi desa'); + } finally { + setIsLoading(false); + } }; - const handleBack = () => router.back(); + loadData(); - // Loading state - if (visiMisiState.findUnique.loading || visiMisiState.update.loading) { - return ( - -
- Memuat data... -
-
- ); + return () => { + stateProfileDesa.visiMisiDesa.update.reset(); + stateProfileDesa.visiMisiDesa.findUnique.reset(); + }; + }, [params?.id, router]); + + // 🔄 Reset Form + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit + const handleSubmit = async () => { + if (!formData.visi.trim()) { + toast.error('Visi wajib diisi'); + return; } - // Error state - if (visiMisiState.findUnique.error) { - return ( - - - - } color="red"> - Error - {visiMisiState.findUnique.error} - - - - ); - } + setIsSubmitting(true); + try { + const originalState = stateProfileDesa.visiMisiDesa; + // update data form ke state sebelum submit + originalState.update.form.visi = formData.visi; + originalState.update.form.misi = formData.misi; + + const success = await originalState.update.submit(); + + if (success) { + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profile/profile-desa'); + } else { + toast.error('Gagal menyimpan data'); + } + } catch (error) { + console.error('Error update visi misi desa:', error); + toast.error('Terjadi kesalahan saat update visi misi desa'); + } finally { + setIsSubmitting(false); + } + }; + + // 🧭 Field handlers + const handleVisiChange = (html: string) => setFormData(prev => ({ ...prev, visi: html })); + const handleMisiChange = (html: string) => setFormData(prev => ({ ...prev, misi: html })); + const handleBack = () => router.back(); + + // ⏳ Loading + if (isLoading) { return ( - - - - - - - Edit Visi Misi Desa - - - - - Edit Visi Misi Desa - - {/* Visi */} - - Visi - visiMisiState.update.form.visi = val} - /> - - - {/* Misi */} - - Misi - visiMisiState.update.form.misi = val} - /> - - - {/* Buttons */} - - - - - - - - - + +
+ + + + Memuat data... + + +
+
); + } + + // ❌ Error + if (loadError) { + return ( + + + + } + color="red" + title="Terjadi Kesalahan" + radius="md" + > + {loadError} + + + + + ); + } + + // ✅ UI + return ( + + + {/* Header */} + + + + Edit Visi & Misi Desa + + + + {/* Form */} + + + + + Informasi Visi & Misi Desa + + + + {/* Visi */} + + + Visi + + + + + {/* Misi */} + + + Misi + + + + + {/* Actions */} + + + + + + + + + ); } export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx index 218819e3..14a4fd5b 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx @@ -1,12 +1,12 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core'; -import { useSnapshot } from 'valtio'; -import stateProfileDesa from '../../../_state/desa/profile'; -import { useEffect } from 'react'; +import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'; import { IconEdit } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { useSnapshot } from 'valtio'; +import stateProfileDesa from '../../../_state/desa/profile'; function Page() { const router = useRouter(); @@ -37,7 +37,6 @@ function Page() { Preview Sejarah Desa - - @@ -70,7 +68,7 @@ function Page() {
- +
@@ -84,7 +82,6 @@ function Page() { Preview Visi Misi Desa - - @@ -118,9 +114,9 @@ function Page() {
Visi Desa - + Misi Desa - +
@@ -134,7 +130,6 @@ function Page() { Preview Lambang Desa - - @@ -162,12 +156,12 @@ function Page() { style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }} > - Lambang Desa + {lambang.judul} - +
@@ -181,7 +175,6 @@ function Page() { Preview Maskot Desa - - @@ -214,9 +206,9 @@ function Page() { - + - + {maskot.images.map((img, idx) => (
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx index f48a8bba..d7c3b632 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx @@ -4,6 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, @@ -13,7 +14,7 @@ import { Text, TextInput, Title, - Tooltip + Loader } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -26,15 +27,27 @@ function EditPerbekelDariMasaKeMasa() { const state = useProxy(stateProfileDesa.mantanPerbekel); const router = useRouter(); const params = useParams(); + const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const [formData, setFormData] = useState({ - nama: state.update.form.nama || '', - daerah: state.update.form.daerah || '', - periode: state.update.form.periode || '', - imageId: state.update.form.imageId || '' + nama: '', + daerah: '', + periode: '', + imageId: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + nama: '', + daerah: '', + periode: '', + imageId: '', + imageUrl: "", + }); + + // load data pertama kali useEffect(() => { const loadFoto = async () => { const id = params?.id as string; @@ -48,7 +61,14 @@ function EditPerbekelDariMasaKeMasa() { periode: data.periode || '', imageId: data.imageId || '' }); - if (data?.imageGalleryFoto?.link) setPreviewImage(data.imageGalleryFoto.link); + setOriginalData({ + nama: data.nama || '', + daerah: data.daerah || '', + periode: data.periode || '', + imageId: data.imageId || '', + imageUrl: data.image.link || '', + }) + setPreviewImage(data.image.link); } } catch (error) { console.error('Error loading foto:', error); @@ -58,8 +78,31 @@ function EditPerbekelDariMasaKeMasa() { loadFoto(); }, [params?.id]); + // helper ubah state formData + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + daerah: originalData.daerah, + periode: originalData.periode, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); + // update global state hanya sekali pas submit state.update.form = { ...state.update.form, ...formData }; if (file) { @@ -78,17 +121,17 @@ function EditPerbekelDariMasaKeMasa() { } catch (error) { console.error('Error updating perbekel dari masa ke masa:', error); toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa'); + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Perbekel Dari Masa Ke Masa @@ -107,7 +150,7 @@ function EditPerbekelDariMasaKeMasa() { label="Nama" placeholder="Masukkan nama" value={formData.nama} - onChange={(e) => setFormData({ ...formData, nama: e.target.value })} + onChange={(e) => handleChange('nama', e.target.value)} required /> @@ -125,7 +168,7 @@ function EditPerbekelDariMasaKeMasa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -144,25 +187,45 @@ function EditPerbekelDariMasaKeMasa() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -171,7 +234,7 @@ function EditPerbekelDariMasaKeMasa() { label="Daerah" placeholder="Masukkan daerah" value={formData.daerah} - onChange={(e) => setFormData({ ...formData, daerah: e.target.value })} + onChange={(e) => handleChange('daerah', e.target.value)} required /> @@ -179,11 +242,22 @@ function EditPerbekelDariMasaKeMasa() { label="Periode" placeholder="Masukkan periode" value={formData.periode} - onChange={(e) => setFormData({ ...formData, periode: e.target.value })} + onChange={(e) => handleChange('periode', e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx index 048d8494..8a63f508 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -98,7 +98,6 @@ function DetailPerbekelDariMasa() { - - - - diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx index ff5f7104..4a8f6aaa 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx @@ -2,7 +2,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Loader, ActionIcon, Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -14,6 +14,7 @@ function CreatePerbekelDariMasaKeMasa() { const state = useProxy(stateProfileDesa.mantanPerbekel); const router = useRouter(); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [file, setFile] = useState(null); const resetForm = () => { @@ -28,33 +29,39 @@ function CreatePerbekelDariMasaKeMasa() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) return toast.error('Gagal upload gambar'); + + state.create.form.imageId = uploaded.id; + await state.create.create(); + resetForm(); + router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan perbekel dari masa ke masa'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) return toast.error('Gagal upload gambar'); - - state.create.form.imageId = uploaded.id; - await state.create.create(); - resetForm(); - router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); }; return ( {/* Back button + Title */} - - - + Create Perbekel Dari Masa Ke Masa @@ -70,21 +77,21 @@ function CreatePerbekelDariMasaKeMasa() { > Nama Perbekel} + label="Nama Perbekel" placeholder="Masukkan nama perbekel" value={state.create.form.nama} onChange={(e) => (state.create.form.nama = e.target.value)} required /> Daerah} + label="Daerah" placeholder="Masukkan daerah" value={state.create.form.daerah} onChange={(e) => (state.create.form.daerah = e.target.value)} required /> Periode} + label="Periode" placeholder="Masukkan periode" value={state.create.form.periode} onChange={(e) => (state.create.form.periode = e.target.value)} @@ -104,7 +111,7 @@ function CreatePerbekelDariMasaKeMasa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -120,25 +127,63 @@ function CreatePerbekelDariMasaKeMasa() { - Seret gambar atau klik untuk memilih file (maks 5MB) + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx index 01367ad9..aaaac6d3 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -49,7 +49,6 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) { List Perbekel Dari Masa Ke Masa - - @@ -75,22 +73,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/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx index 43eefc8a..1a916bfe 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx @@ -4,9 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconPhoto, IconUpload, IconX, IconImageInPicture } from '@tabler/icons-react'; +import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; @@ -101,11 +101,9 @@ function ProfilePerbekel() { {/* Header */} - - Edit Profil Perbekel @@ -128,8 +126,8 @@ function ProfilePerbekel() { handleFileChange(files[0])} onReject={() => toast.error('File tidak valid.')} - maxSize={5 * 1024 ** 2} // 5MB - accept={{ 'image/*': [] }} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx index 1f435fea..2d2ddf4a 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx @@ -1,7 +1,7 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { IconEdit } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; @@ -36,7 +36,6 @@ function Page() { Preview Profil PPID - - @@ -97,16 +95,16 @@ function Page() { {/* Biodata & Info */} Biodata - + Pengalaman - + Pengalaman Organisasi - + Program Kerja Unggulan - + diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx index dca2dfe2..ff6d98fc 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx @@ -2,23 +2,22 @@ 'use client' import colors from '@/con/colors'; import { + ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip, - ScrollArea, + Title } from '@mantine/core'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; import { - IconFileAnalytics, IconCoins, + IconFileAnalytics, IconShoppingCart, IconWallet, } from '@tabler/icons-react'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -29,29 +28,25 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "APB Desa", value: "apbdesa", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa", - icon: , - tooltip: "Lihat ringkasan Anggaran Pendapatan dan Belanja Desa", + icon: }, { label: "Pendapatan", value: "pendapatan", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan", icon: , - tooltip: "Kelola data pendapatan desa", }, { label: "Belanja", value: "belanja", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja", icon: , - tooltip: "Atur data belanja desa", }, { label: "Pembiayaan", value: "pembiayaan", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan", icon: , - tooltip: "Kelola data pembiayaan desa", }, ]; @@ -104,26 +99,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx index 7d07fd77..121be66a 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx @@ -1,11 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -'use client' /* eslint-disable react-hooks/exhaustive-deps */ +'use client'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import colors from '@/con/colors'; import { + Alert, Box, Button, + Group, + Loader, MultiSelect, Paper, Skeleton, @@ -13,8 +16,6 @@ import { Text, TextInput, Title, - Tooltip, - Group, } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; @@ -23,76 +24,132 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +// ==================== HELPERS ==================== +const safeStringArray = (arr: any[]): string[] => { + if (!Array.isArray(arr)) return []; + return arr + .filter(item => item != null && item !== '') + .map(item => String(item)) + .filter(item => item.trim() !== ''); +}; + +const createEmptyForm = () => ({ + tahun: '', + pendapatanIds: [] as string[], + belanjaIds: [] as string[], + pembiayaanIds: [] as string[], +}); + +// ==================== COMPONENT ==================== function EditAPBDesa() { const apbState = useProxy(PendapatanAsliDesa.ApbDesa); const router = useRouter(); const params = useParams(); - const [formData, setFormData] = useState({ - tahun: apbState.update.form.tahun || '', - pendapatanIds: apbState.update.form.pendapatanIds || [], - belanjaIds: apbState.update.form.belanjaIds || [], - pembiayaanIds: apbState.update.form.pembiayaanIds || [], - }); + const [formData, setFormData] = useState(createEmptyForm()); + const [originalData, setOriginalData] = useState(createEmptyForm()); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(true); - // Load APB desa by id + // ==================== LOAD DATA ==================== useEffect(() => { const loadAPBdesa = async () => { const id = params?.id as string; - if (!id) return; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + return; + } try { + setIsLoading(true); const data = await apbState.update.load(id); - if (data) { - setFormData({ - tahun: data.tahun || 0, - pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [], - belanjaIds: data.belanja?.map((b: any) => b.id) || [], - pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [], - }); + + if (!data) { + toast.error('Data APB Desa tidak ditemukan'); + return; } - } catch (error) { - console.error("Error loading APBdesa:", error); - toast.error("Gagal memuat data APBdesa"); + + const normalized = { + tahun: String(data.tahun || ''), + pendapatanIds: safeStringArray(data.pendapatan?.map((p: any) => p.id) || []), + belanjaIds: safeStringArray(data.belanja?.map((b: any) => b.id) || []), + pembiayaanIds: safeStringArray(data.pembiayaan?.map((p: any) => p.id) || []), + }; + + setFormData(normalized); + setOriginalData(normalized); + } catch (err) { + console.error('Error loading APBdesa:', err); + toast.error('Gagal memuat data APB Desa'); + } finally { + setIsLoading(false); } }; loadAPBdesa(); }, [params?.id]); + // ==================== HANDLERS ==================== + const handleChange = (field: keyof typeof formData, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); + + if (!formData.tahun.trim()) { + toast.warning('Tahun harus diisi'); + return; + } + apbState.update.form = { ...apbState.update.form, tahun: Number(formData.tahun), - pendapatanIds: formData.pendapatanIds, - belanjaIds: formData.belanjaIds, - pembiayaanIds: formData.pembiayaanIds, + pendapatanIds: safeStringArray(formData.pendapatanIds), + belanjaIds: safeStringArray(formData.belanjaIds), + pembiayaanIds: safeStringArray(formData.pembiayaanIds), }; await apbState.update.update(); - toast.success("APB Desa berhasil diperbarui!"); - router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"); + toast.success('APB Desa berhasil diperbarui!'); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); } catch (error) { - console.error("Error updating APBdesa:", error); - toast.error("Terjadi kesalahan saat memperbarui APBdesa"); + console.error('Error updating APBdesa:', error); + toast.error('Terjadi kesalahan saat memperbarui APB Desa'); + } finally { + setIsSubmitting(false); } }; + // ==================== UI ==================== + if (isLoading) { + return ( + + + Memuat data APB Desa... + + ); + } + return ( {/* Header */} - - - + Edit APB Desa @@ -112,38 +169,46 @@ function EditAPBDesa() { - setFormData({ ...formData, tahun: e.target.value }) - } + onChange={(e) => handleChange('tahun', e.target.value)} label={Tahun} placeholder="Masukkan tahun anggaran" required /> {/* Selects */} - - setFormData({ ...formData, pendapatanIds: ids }) - } + onSelectionChange={(ids) => handleChange('pendapatanIds', ids)} /> - - setFormData({ ...formData, belanjaIds: ids }) - } + onSelectionChange={(ids) => handleChange('belanjaIds', ids)} /> - - setFormData({ ...formData, pembiayaanIds: ids }) - } + onSelectionChange={(ids) => handleChange('pembiayaanIds', ids)} /> {/* Save Button */} - + + + ); +} - /* --- Sub Components --- */ +// ==================== SUB COMPONENT ==================== +function SelectAPBItem({ + label, + state, + selectedIds, + onSelectionChange, +}: { + label: string; + state: any; + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; +}) { + const proxyState = useProxy(state); - function SelectPendapatan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) { - const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); + useShallowEffect(() => { + proxyState.findMany.load(); + }, []); - useShallowEffect(() => { - pendapatanState.findMany.load(); - }, []); + const data = proxyState.findMany.data; + const isLoading = !data; - if (!pendapatanState.findMany.data) { - return ; - } + const options = + data + ?.filter((item: any) => item?.id) + .map((item: any) => ({ + value: String(item.id), + label: String(item?.name || '(Tanpa Nama)'), + })) || []; + if (isLoading) { return ( - Pendapatan} - data={pendapatanState.findMany.data.map((p: any) => ({ - value: p.id, - label: p.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih pendapatan..." - nothingFoundMessage="Tidak ditemukan" - /> + + {label} + + ); } - function SelectBelanja({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) { - const belanjaState = useProxy(PendapatanAsliDesa.belanja); - - useShallowEffect(() => { - belanjaState.findMany.load(); - }, []); - - if (!belanjaState.findMany.data) { - return ; - } - + if (options.length === 0) { return ( - Belanja} - data={belanjaState.findMany.data.map((b: any) => ({ - value: b.id, - label: b.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih belanja..." - nothingFoundMessage="Tidak ditemukan" - /> + + + Tidak ada data {label.toLowerCase()} tersedia. + + ); } - function SelectPembiayaan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) { - const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); - - useShallowEffect(() => { - pembiayaanState.findMany.load(); - }, []); - - if (!pembiayaanState.findMany.data) { - return ; - } - - return ( - Pembiayaan} - data={pembiayaanState.findMany.data.map((p: any) => ({ - value: p.id, - label: p.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih pembiayaan..." - nothingFoundMessage="Tidak ditemukan" - /> - ); - } + return ( + {label}} + data={options} + value={selectedIds} + onChange={(ids) => onSelectionChange(safeStringArray(ids))} + searchable + clearable + placeholder={`Pilih ${label.toLowerCase()}...`} + nothingFoundMessage="Tidak ditemukan" + /> + ); } export default EditAPBDesa; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx index ee57dc50..67bc460f 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx @@ -9,8 +9,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -81,7 +80,7 @@ function DetailAPBDesa() { Detail APB Desa - + @@ -159,36 +158,32 @@ function DetailAPBDesa() { - - - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx index 174d0c9a..ad347bc2 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx @@ -6,23 +6,26 @@ import { Box, Button, Group, + Loader, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateAPBDesa() { const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { apbDesaState.create.form = { @@ -34,20 +37,26 @@ function CreateAPBDesa() { }; const handleSubmit = async () => { - await apbDesaState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + try { + setIsSubmitting(true); + await apbDesaState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + } catch (error) { + console.error('Error creating APB Desa:', error); + toast.error('Gagal membuat APB Desa'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah APB Desa @@ -97,6 +106,17 @@ function CreateAPBDesa() { {/* Action */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx index ff0c6464..e87b21f1 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx @@ -5,8 +5,8 @@ import { Button, Center, Group, - Paper, Pagination, + Paper, Skeleton, Stack, Table, @@ -15,8 +15,7 @@ import { TableTh, TableThead, TableTr, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -82,20 +81,18 @@ function ListAPBDesa({ search }: { search: string }) { List APB Desa - - - +
@@ -138,20 +135,18 @@ function ListAPBDesa({ search }: { search: string }) { )} - - - + )) diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx index ea3729d2..533a12db 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx @@ -1,16 +1,17 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' + import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,14 +23,24 @@ function EditBelanja() { const belanjaState = useProxy(PendapatanAsliDesa.belanja); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - name: belanjaState.update.form.name || '', - value: belanjaState.update.form.value || '', + name: '', + value: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + value: '', + }); + + // format angka ke rupiah const formatRupiah = (value: number | string) => { - const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); + const number = + typeof value === 'number' + ? value + : Number(value.replace(/\D/g, '')) || 0; return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', @@ -37,8 +48,9 @@ function EditBelanja() { }).format(number); }; + // buang semua simbol jadi angka murni const unformatRupiah = (value: string) => { - return Number(value.replace(/\D/g, '')); + return Number(value.replace(/\D/g, '')) || 0; }; useEffect(() => { @@ -51,7 +63,11 @@ function EditBelanja() { if (data) { setFormData({ name: data.name || '', - value: data.value || '', + value: String(data.value || ''), + }); + setOriginalData({ + name: data.name || '', + value: String(data.value || ''), }); } } catch (error) { @@ -65,11 +81,12 @@ function EditBelanja() { const handleSubmit = async () => { try { + setIsSubmitting(true); belanjaState.update.form = { ...belanjaState.update.form, name: formData.name, value: Number(formData.value), - } + }; await belanjaState.update.update(); toast.success("Jenis Belanja berhasil diperbarui!"); @@ -77,23 +94,31 @@ function EditBelanja() { } catch (error) { console.error("Error updating jenis belanja:", error); toast.error("Terjadi kesalahan saat memperbarui jenis belanja"); + } finally { + setIsSubmitting(false); } - } + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info("Form dikembalikan ke data awal"); + }; return ( {/* Header */} - - - + Edit Jenis Belanja @@ -113,23 +138,36 @@ function EditBelanja() { label="Nama Jenis Belanja" placeholder="Masukkan nama jenis belanja" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => + setFormData({ ...formData, name: e.target.value }) + } required /> { const raw = e.currentTarget.value; const cleanValue = unformatRupiah(raw); - setFormData({ ...formData, value: cleanValue }); + setFormData({ ...formData, value: String(cleanValue) }); }} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx index 76bc332c..fee6e242 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx @@ -6,21 +6,23 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; +import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateBelanja() { const belanjaState = useProxy(PendapatanAsliDesa.belanja); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const formatRupiah = (value: number | string) => { const number = @@ -48,25 +50,31 @@ function CreateBelanja() { return toast.warn('Lengkapi semua field terlebih dahulu'); } - await belanjaState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja'); + try { + setIsSubmitting(true); + await belanjaState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja'); + } catch (error) { + console.error('Error creating belanja:', error); + toast.error('Gagal menambahkan jenis belanja'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan back button */} - - - + Tambah Jenis Belanja @@ -103,6 +111,17 @@ function CreateBelanja() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx index 467c8142..1a09d875 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx @@ -17,8 +17,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; @@ -96,18 +95,16 @@ function ListBelanja({ search }: { search: string }) { Daftar Belanja - - - + @@ -138,34 +135,30 @@ function ListBelanja({ search }: { search: string }) { - - - - - - + + diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx index 0492b8ee..c7e642dd 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,14 +22,23 @@ function EditPembiayaan() { const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - name: pembiayaanState.update.form.name || '', - value: pembiayaanState.update.form.value || '', + name: '', + value: '', + }); + + const [originalData, setOriginalData] = useState({ + name: '', + value: '', }); const formatRupiah = (value: number | string) => { - const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, '')); + const number = + typeof value === 'number' + ? value + : Number(value.toString().replace(/\D/g, '')) || 0; return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', @@ -38,7 +47,7 @@ function EditPembiayaan() { }; const unformatRupiah = (value: string) => { - return Number(value.replace(/\D/g, '')); + return Number(value.replace(/\D/g, '')) || 0; }; useEffect(() => { @@ -51,7 +60,11 @@ function EditPembiayaan() { if (data) { setFormData({ name: data.name || '', - value: data.value || '', + value: String(data.value || ''), + }); + setOriginalData({ + name: data.name || '', + value: String(data.value || ''), }); } } catch (error) { @@ -63,12 +76,21 @@ function EditPembiayaan() { loadPembiayaan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); pembiayaanState.update.form = { ...pembiayaanState.update.form, name: formData.name, - value: Number(formData.value), + value: unformatRupiah(formData.value), }; await pembiayaanState.update.update(); @@ -77,23 +99,23 @@ function EditPembiayaan() { } catch (error) { console.error('Error updating jenis pembiayaan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan'); + } finally { + setIsSubmitting(false); } }; return ( - {/* Header dengan Back Button */} + {/* Header */} - - - + Edit Jenis Pembiayaan @@ -113,23 +135,36 @@ function EditPembiayaan() { label="Nama Jenis Pembiayaan" placeholder="Masukkan nama jenis pembiayaan" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => + setFormData((prev) => ({ ...prev, name: e.target.value })) + } required /> { const raw = e.currentTarget.value; const cleanValue = unformatRupiah(raw); - setFormData({ ...formData, value: cleanValue }); + setFormData((prev) => ({ ...prev, value: String(cleanValue) })); }} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx index ff6e17ff..6b808b45 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx @@ -1,26 +1,27 @@ 'use client'; -import React from 'react'; -import { useProxy } from 'valtio/utils'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; -import { useRouter } from 'next/navigation'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, - Title, - TextInput, Text, - Tooltip, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreatePembiayaan() { const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const formatRupiah = (value: number | string) => { const number = @@ -44,29 +45,35 @@ function CreatePembiayaan() { }; const handleSubmit = async () => { - if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) { - return toast.warn('Nama dan nilai wajib diisi'); - } + try { + setIsSubmitting(true); + if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) { + return toast.warn('Nama dan nilai wajib diisi'); + } - await pembiayaanState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan'); + await pembiayaanState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan'); + } catch (error) { + console.error('Error creating pembiayaan:', error); + toast.error('Gagal menambahkan jenis pembiayaan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Jenis Pembiayaan @@ -105,6 +112,17 @@ function CreatePembiayaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx index 627f5894..f9c265a6 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx @@ -1,9 +1,11 @@ 'use client' +import colors from '@/con/colors'; import { Box, Button, Center, Group, + Pagination, Paper, Skeleton, Stack, @@ -14,19 +16,16 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, - Pagination, + Title } from '@mantine/core'; -import React, { useState } from 'react'; -import HeaderSearch from '../../../_com/header'; -import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; -import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; -import { useProxy } from 'valtio/utils'; -import { useRouter } from 'next/navigation'; import { useShallowEffect } from '@mantine/hooks'; -import colors from '@/con/colors'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; function Pembiayaan() { const [search, setSearch] = useState(""); @@ -95,18 +94,16 @@ function ListPembiayaan({ search }: { search: string }) { Daftar Pembiayaan - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx index 1970c267..0ae681e0 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,14 +22,24 @@ function EditPendapatan() { const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); - const [formData, setFormData] = useState({ - name: pendapatanState.update.form.name || '', - value: pendapatanState.update.form.value || '', + const [originalData, setOriginalData] = useState({ + name: "", + value: "", }); + const [formData, setFormData] = useState({ + name: '', + value: '', + }); + + // helper format const formatRupiah = (value: number | string) => { - const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, '')); + const number = typeof value === 'number' + ? value + : Number(value.toString().replace(/\D/g, '')); + return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', @@ -39,6 +49,7 @@ function EditPendapatan() { const unformatRupiah = (value: string) => Number(value.replace(/\D/g, '')); + // load data once useEffect(() => { const id = params?.id as string; if (!id) return; @@ -48,8 +59,12 @@ function EditPendapatan() { const data = await pendapatanState.update.load(id); if (data) { setFormData({ - name: data.name || '', - value: data.value || '', + name: data.name ?? '', + value: data.value?.toString() ?? '', + }); + setOriginalData({ + name: data.name ?? '', + value: data.value?.toString() ?? '', }); } } catch (error) { @@ -61,8 +76,25 @@ function EditPendapatan() { loadPendapatan(); }, [params?.id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); pendapatanState.update.form = { ...pendapatanState.update.form, name: formData.name, @@ -75,23 +107,24 @@ function EditPendapatan() { } catch (error) { console.error('Error updating jenis pendapatan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan'); + } finally { + setIsSubmitting(false); } + }; return ( {/* Header with Back Button */} - - - + Edit Jenis Pendapatan @@ -111,23 +144,34 @@ function EditPendapatan() { label="Nama Jenis Pendapatan" placeholder="Masukkan nama jenis pendapatan" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange('name', e.target.value)} required /> { const raw = e.currentTarget.value; - const cleanValue = unformatRupiah(raw); - setFormData({ ...formData, value: cleanValue }); + const cleanValue = unformatRupiah(raw).toString(); + handleChange('value', cleanValue); }} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx index 9ff6882f..75fc530f 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx @@ -5,20 +5,22 @@ import { Box, Button, Group, + Loader, Paper, Stack, - Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreatePendapatan() { const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const formatRupiah = (value: number | string) => { const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); @@ -41,25 +43,31 @@ function CreatePendapatan() { }; const handleSubmit = async () => { - await pendapatanState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan'); + try { + setIsSubmitting(true); + await pendapatanState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan'); + } catch (error) { + console.error('Error creating pendapatan:', error); + toast.error('Gagal menambahkan jenis pendapatan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan tombol back + judul */} - - - + Tambah Jenis Pendapatan @@ -80,11 +88,7 @@ function CreatePendapatan() { onChange={(val) => { pendapatanState.create.form.name = val.target.value; }} - label={ - - Nama Jenis Pendapatan - - } + label="Nama Jenis Pendapatan" placeholder="Masukkan nama jenis pendapatan" required /> @@ -97,16 +101,23 @@ function CreatePendapatan() { const cleanValue = unformatRupiah(raw); pendapatanState.create.form.value = cleanValue; }} - label={ - - Nilai - - } + label="Nilai" placeholder="Masukkan nilai" required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx index 47ba7b86..9c0fe175 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx @@ -17,8 +17,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; @@ -96,18 +95,16 @@ function ListPendapatan({ search }: { search: string }) { Daftar Pendapatan - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx index 20e7e339..31f4eb71 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx @@ -6,56 +6,114 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; -function EditDemografiPekerjaan() { +interface FormData { + pekerjaan: string; + lakiLaki: number; + perempuan: number; +} + +export default function EditDemografiPekerjaan() { const router = useRouter(); - const params = useParams() as { id: string }; + const { id } = useParams() as { id: string }; const stateDemografi = useProxy(demografiPekerjaan); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + pekerjaan: '', + lakiLaki: 0, + perempuan: 0, + }); + const [originalData, setOriginalData] = useState({ + pekerjaan: '', + lakiLaki: 0, + perempuan: 0, + }); - const id = params.id; - + // ✅ Load data hanya sekali di awal (tidak reset form) useEffect(() => { if (!id) return; - stateDemografi.update.id = id; - stateDemografi.findUnique - .load(id) - .then(() => { + + const loadData = async () => { + try { + setIsSubmitting(true); + stateDemografi.update.id = id; + await stateDemografi.findUnique.load(id); + const data = stateDemografi.findUnique.data; if (data) { - stateDemografi.update.form = { - pekerjaan: String(data.pekerjaan || ''), - lakiLaki: Number(data.lakiLaki || 0), - perempuan: Number(data.perempuan || 0), - }; + setFormData({ + pekerjaan: data.pekerjaan ?? '', + lakiLaki: Number(data.lakiLaki ?? 0), + perempuan: Number(data.perempuan ?? 0), + }); + setOriginalData({ + pekerjaan: data.pekerjaan ?? '', + lakiLaki: Number(data.lakiLaki ?? 0), + perempuan: Number(data.perempuan ?? 0), + }); } - }) - .catch((error) => { + } catch (error) { console.error('Error loading data:', error); toast.error('Gagal memuat data'); - }); + } finally { + setIsSubmitting(false); + } + }; + + loadData(); }, [id]); + // ✅ Handler input terkontrol (tidak buat re-render berlebihan) + const handleChange = useCallback( + (field: keyof FormData) => + (e: React.ChangeEvent) => { + const value = + field === 'lakiLaki' || field === 'perempuan' + ? Number(e.currentTarget.value) + : e.currentTarget.value; + + setFormData((prev) => ({ ...prev, [field]: value })); + }, + [] + ); + + const handleResetForm = () => { + setFormData({ + pekerjaan: originalData.pekerjaan, + lakiLaki: Number(originalData.lakiLaki), + perempuan: Number(originalData.perempuan), + }); + toast.info("Form dikembalikan ke data awal"); + }; + + // ✅ Submit hanya update global state sekali const handleSubmit = async () => { try { + setIsSubmitting(true); stateDemografi.update.id = id; + stateDemografi.update.form = { ...formData }; + await stateDemografi.update.submit(); + toast.success('Data berhasil diperbarui'); router.push('/admin/ekonomi/demografi-pekerjaan'); } catch (error) { console.error('Error updating data:', error); toast.error('Gagal memperbarui data'); + } finally { + setIsSubmitting(false); } }; @@ -63,16 +121,14 @@ function EditDemografiPekerjaan() { {/* Header */} - - - + Edit Demografi Pekerjaan @@ -91,10 +147,8 @@ function EditDemografiPekerjaan() { - (stateDemografi.update.form.pekerjaan = e.currentTarget.value) - } + value={formData.pekerjaan} + onChange={handleChange('pekerjaan')} required /> @@ -102,12 +156,8 @@ function EditDemografiPekerjaan() { label="Jumlah Pekerja Laki-laki" type="number" placeholder="Masukkan jumlah pekerja laki-laki" - value={stateDemografi.update.form.lakiLaki} - onChange={(e) => - (stateDemografi.update.form.lakiLaki = Number( - e.currentTarget.value - )) - } + value={formData.lakiLaki} + onChange={handleChange('lakiLaki')} required /> @@ -115,16 +165,23 @@ function EditDemografiPekerjaan() { label="Jumlah Pekerja Perempuan" type="number" placeholder="Masukkan jumlah pekerja perempuan" - value={stateDemografi.update.form.perempuan} - onChange={(e) => - (stateDemografi.update.form.perempuan = Number( - e.currentTarget.value - )) - } + value={formData.perempuan} + onChange={handleChange('perempuan')} required /> + + + {/* Tombol Simpan */} @@ -143,5 +200,3 @@ function EditDemografiPekerjaan() { ); } - -export default EditDemografiPekerjaan; diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx index f7ef972e..4d280abf 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx @@ -7,22 +7,24 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; +import { toast } from 'react-toastify'; function CreateDemografiPekerjaan() { const stateDemografi = useProxy(demografiPekerjaan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateDemografi.create.form = { @@ -33,32 +35,37 @@ function CreateDemografiPekerjaan() { }; const handleSubmit = async () => { - const id = await stateDemografi.create.create(); - if (id) { - const idStr = String(id); - await stateDemografi.findUnique.load(idStr); - if (stateDemografi.findUnique.data) { - setChartData([stateDemografi.findUnique.data]); + try { + const id = await stateDemografi.create.create(); + if (id) { + const idStr = String(id); + await stateDemografi.findUnique.load(idStr); + if (stateDemografi.findUnique.data) { + setChartData([stateDemografi.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/demografi-pekerjaan'); + } catch (error) { + console.error('Error creating demografi pekerjaan:', error); + toast.error('Terjadi kesalahan saat menambah demografi pekerjaan'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/demografi-pekerjaan'); }; return ( {/* Header */} - - - + Tambah Demografi Pekerjaan @@ -106,6 +113,17 @@ function CreateDemografiPekerjaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx index cc679025..ea4af5a2 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; import { BarChart } from '@mantine/charts'; @@ -5,7 +6,9 @@ import { Box, Button, Center, + Flex, Group, + Pagination, Paper, Skeleton, Stack, @@ -16,10 +19,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, - Pagination, - Flex, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; @@ -85,7 +85,7 @@ function ListDemografiPekerjaan({ search }: { search: string }) { useEffect(() => { if (data) { setChartData( - data.map((item) => ({ + data.map((item: any) => ({ id: item.id, pekerjaan: item.pekerjaan, lakiLaki: Number(item.lakiLaki), @@ -110,25 +110,23 @@ function ListDemografiPekerjaan({ search }: { search: string }) { List Demografi Pekerjaan - - - +
- Pekerjaan - Laki - Laki - Perempuan + Pekerjaan + Laki - Laki + Perempuan Edit Hapus @@ -137,9 +135,9 @@ function ListDemografiPekerjaan({ search }: { search: string }) { {filteredData.length > 0 ? ( filteredData.map((item) => ( - {item.pekerjaan} - {item.lakiLaki} - {item.perempuan} + {item.pekerjaan} + {item.lakiLaki} + {item.perempuan} - + Edit Jumlah Penduduk Miskin @@ -78,10 +129,8 @@ function EditJumlahPendudukMiskin() { placeholder="Masukkan tahun" type="number" required - value={stateJPM.update.form.year} - onChange={(val) => { - stateJPM.update.form.year = Number(val.currentTarget.value); - }} + value={formData.year} + onChange={(e) => handleChange('year', e.currentTarget.value)} /> { - stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value); - }} + value={formData.totalPoorPopulation} + onChange={(e) => + handleChange('totalPoorPopulation', e.currentTarget.value) + } /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx index d045d1d6..363688f5 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx @@ -1,18 +1,20 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; -import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; +import colors from '@/con/colors'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import colors from '@/con/colors'; import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin'; +import { toast } from 'react-toastify'; export default function CreateJumlahPendudukMiskin() { const stateJPM = useProxy(jumlahPendudukMiskin); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateJPM.create.form = { @@ -22,27 +24,33 @@ export default function CreateJumlahPendudukMiskin() { }; const handleSubmit = async () => { - const id = await stateJPM.create.create(); - if (id) { - const idStr = String(id); - await stateJPM.findUnique.load(idStr); - if (stateJPM.findUnique.data) { - setChartData([stateJPM.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stateJPM.create.create(); + if (id) { + const idStr = String(id); + await stateJPM.findUnique.load(idStr); + if (stateJPM.findUnique.data) { + setChartData([stateJPM.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/jumlah-penduduk-miskin'); + } catch (error) { + console.error(error) + toast.error(error instanceof Error ? error.message : "Gagal menambahkan jumlah penduduk miskin") + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/jumlah-penduduk-miskin'); }; return ( {/* Header */} - - - + Tambah Jumlah Penduduk Miskin @@ -82,6 +90,17 @@ export default function CreateJumlahPendudukMiskin() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx index 498a8ce4..c588820c 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx @@ -1,23 +1,42 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; -import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; -import HeaderSearch from '../../_com/header'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title +} from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { useShallowEffect, useMediaQuery } from '@mantine/hooks'; import { useProxy } from 'valtio/utils'; -import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin'; -import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip as RechartsTooltip } from 'recharts'; +import HeaderSearch from '../../_com/header'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; +import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin'; + +// ✅ BarChart Mantine +import { BarChart } from '@mantine/charts'; function JumlahPendudukMiskin() { const [search, setSearch] = useState(""); return ( } value={search} onChange={(e) => setSearch(e.currentTarget.value)} @@ -28,7 +47,7 @@ function JumlahPendudukMiskin() { } function ListJumlahPendudukMiskin({ search }: { search: string }) { - type JPMGrafik = { id: string; year: number; totalPoorPopulation: number } + type JPMGrafik = { year: number; totalPoorPopulation: number }; const stateJPM = useProxy(jumlahPendudukMiskin); const [chartData, setChartData] = useState([]); const [mounted, setMounted] = useState(false); @@ -36,33 +55,27 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { const [selectedId, setSelectedId] = useState(null); const router = useRouter(); - const isTablet = useMediaQuery('(max-width:1024px)'); - const isMobile = useMediaQuery('(max-width:768px)'); + const { data, page, loading, load, totalPages } = stateJPM.findMany; - const { - data, - page, - loading, - load, - totalPages, - } = stateJPM.findMany; - // Load data + // Load data awal useShallowEffect(() => { setMounted(true); load(page, 10, search); }, [page, search]); + // Update chart data useEffect(() => { if (stateJPM.findMany.data) { - setChartData(stateJPM.findMany.data.map(item => ({ - id: item.id, - year: Number(item.year), - totalPoorPopulation: Number(item.totalPoorPopulation) - }))); + setChartData( + stateJPM.findMany.data.map((item) => ({ + year: Number(item.year), + totalPoorPopulation: Number(item.totalPoorPopulation), + })) + ); } }, [stateJPM.findMany.data]); - const filteredData = data || [] + const filteredData = data || []; const handleDelete = () => { if (selectedId) { @@ -71,7 +84,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { setSelectedId(null); stateJPM.findMany.load(); } - } + }; if (loading || !data) { return ( @@ -83,19 +96,20 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { return ( + {/* Tabel */} Daftar Jumlah Penduduk Miskin - - - + @@ -109,22 +123,38 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { - {filteredData.length > 0 ? filteredData.map(item => ( - - {item.year} - {item.totalPoorPopulation} - - - - - - - - )) : ( + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + {item.year} + {item.totalPoorPopulation} + + + + + + + + )) + ) : (
@@ -138,6 +168,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { + {/* Pagination */}
- {/* Chart */} + {/* Bar Chart */} - - Grafik Jumlah Penduduk Miskin - {mounted && chartData.length > 0 ? ( - - - - - - - - ) : ( - Belum ada data untuk ditampilkan dalam grafik - )} - + + Grafik Jumlah Penduduk Miskin + + {mounted && chartData.length > 0 ? ( + ({ + name: item.year.toString(), + value: item.totalPoorPopulation, + }))} + dataKey="name" + series={[ + { name: 'value', color: colors['blue-button'] }, + ]} + withTooltip + valueFormatter={(v) => `${v.toLocaleString()} jiwa`} + /> + ) : ( + Belum ada data untuk ditampilkan dalam grafik + )} - {/* Modal Hapus */} setModalHapus(false)} onConfirm={handleDelete} - text='Apakah anda yakin ingin menghapus data ini?' + text="Apakah anda yakin ingin menghapus data ini?" /> ); diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx index 8b884c50..ee70c101 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx @@ -2,18 +2,17 @@ 'use client' import colors from '@/con/colors'; import { + ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip, - ScrollArea, + Title } from '@mantine/core'; +import { IconSchool, IconUsers } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconUsers, IconSchool } from '@tabler/icons-react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -24,15 +23,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "Pengangguran Berdasarkan Usia", value: "pengangguranberdasarkanusia", href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia", - icon: , - tooltip: "Data pengangguran menurut kelompok usia", + icon: }, { label: "Pengangguran Berdasarkan Pendidikan", value: "pengangguranberdasarkanpendidikan", href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan", - icon: , - tooltip: "Data pengangguran menurut tingkat pendidikan", + icon: }, ]; @@ -78,26 +75,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx index 5d58837e..e2991004 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx @@ -1,11 +1,12 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; -/* eslint-disable react-hooks/exhaustive-deps */ import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function EditGrafikBerdasarkanPendidikan() { @@ -13,38 +14,95 @@ function EditGrafikBerdasarkanPendidikan() { const params = useParams() as { id: string }; const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const id = params.id; + const [isSubmitting, setIsSubmitting] = useState(false); + + // state lokal untuk form + const [formData, setFormData] = useState({ + SD: '', + SMP: '', + SMA: '', + D3: '', + S1: '', + }); + + const [originalData, setOriginalData] = useState({ + SD: '', + SMP: '', + SMA: '', + D3: '', + S1: '', + }); useEffect(() => { if (id) { stategrafik.findUnique.load(id).then(() => { const data = stategrafik.findUnique.data; if (data) { - stategrafik.update.form = { + setFormData({ SD: data.SD || '', SMP: data.SMP || '', SMA: data.SMA || '', D3: data.D3 || '', S1: data.S1 || '', - }; + }); + setOriginalData({ + SD: data.SD || '', + SMP: data.SMP || '', + SMA: data.SMA || '', + D3: data.D3 || '', + S1: data.S1 || '', + }); } }); } }, [id]); + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.currentTarget; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + + + const handleResetForm = () => { + setFormData({ + SD: originalData.SD, + SMP: originalData.SMP, + SMA: originalData.SMA, + D3: originalData.D3, + S1: originalData.S1, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { - stategrafik.update.id = id; - await stategrafik.update.submit(); - router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'); + try { + setIsSubmitting(true); + stategrafik.update.id = id; + stategrafik.update.form = { ...formData }; // update global state pas submit aja + await stategrafik.update.submit(); + router.push( + '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' + ); + } catch (error) { + console.error(error); + toast.error('Terjadi kesalahan saat memperbarui data grafik'); + } finally { + setIsSubmitting(false); + } }; return ( - - - + Edit Grafik Pengangguran Berdasarkan Pendidikan @@ -60,42 +118,58 @@ function EditGrafikBerdasarkanPendidikan() { > (stategrafik.update.form.SD = val.currentTarget.value)} + value={formData.SD} + onChange={handleChange} /> (stategrafik.update.form.SMP = val.currentTarget.value)} + value={formData.SMP} + onChange={handleChange} /> (stategrafik.update.form.SMA = val.currentTarget.value)} + value={formData.SMA} + onChange={handleChange} /> (stategrafik.update.form.D3 = val.currentTarget.value)} + value={formData.D3} + onChange={handleChange} /> (stategrafik.update.form.S1 = val.currentTarget.value)} + value={formData.S1} + onChange={handleChange} /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx index d62b2de1..c530352d 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx @@ -1,19 +1,20 @@ 'use client'; /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import React from 'react'; -import { useRouter } from 'next/navigation'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; -import { useProxy } from 'valtio/utils'; -import { useState } from 'react'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core'; +import { Box, Button, Loader, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateGrafikBerdasarkanPendidikan() { const router = useRouter(); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const [donutData, setDonutData] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stategrafik.create.form = { @@ -27,28 +28,34 @@ function CreateGrafikBerdasarkanPendidikan() { }; const handleSubmit = async () => { - const id = await stategrafik.create.create(); - if (id) { - const idStr = String(id); - await stategrafik.findUnique.load(idStr); - if (stategrafik.findUnique.data) { - setDonutData([stategrafik.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stategrafik.create.create(); + if (id) { + const idStr = String(id); + await stategrafik.findUnique.load(idStr); + if (stategrafik.findUnique.data) { + setDonutData([stategrafik.findUnique.data]); + } } + resetForm(); + router.push( + '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' + ); + } catch (error) { + console.error(error); + toast.error('Terjadi kesalahan saat menambahkan data grafik'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push( - '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' - ); }; return ( - - - + Tambah Data Pengangguran Berdasarkan Pendidikan @@ -105,6 +112,17 @@ function CreateGrafikBerdasarkanPendidikan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx index 328a19da..825f6358 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx @@ -1,24 +1,41 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; +import { DonutChart } from '@mantine/charts'; +import { + Box, + Button, + Center, + Flex, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title +} from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { Cell, Pie, PieChart } from 'recharts'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; function GrafikBerdasarkanPendidikan() { - const [search, setSearch] = useState(""); + const [search, setSearch] = useState(''); return ( } value={search} onChange={(e) => setSearch(e.currentTarget.value)} @@ -31,7 +48,6 @@ function GrafikBerdasarkanPendidikan() { function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const [donutData, setDonutData] = useState([]); - const [mounted, setMounted] = useState(false); const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); const router = useRouter(); @@ -45,37 +61,45 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { } }; - const { - data, - page, - totalPages, - loading, - load, - } = stategrafik.findMany; + const { data, page, totalPages, loading, load } = stategrafik.findMany; useShallowEffect(() => { - setMounted(true); load(page, 10, search); }, [page, search]); useEffect(() => { if (stategrafik.findMany.data) { - const SD = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SD || 0), 0); - const SMP = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMP || 0), 0); - const SMA = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMA || 0), 0); - const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0); - const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0); + const SD = stategrafik.findMany.data.reduce( + (acc: number, cur: any) => acc + Number(cur.SD || 0), + 0, + ); + const SMP = stategrafik.findMany.data.reduce( + (acc: number, cur: any) => acc + Number(cur.SMP || 0), + 0, + ); + const SMA = stategrafik.findMany.data.reduce( + (acc: number, cur: any) => acc + Number(cur.SMA || 0), + 0, + ); + const D3 = stategrafik.findMany.data.reduce( + (acc: number, cur: any) => acc + Number(cur.D3 || 0), + 0, + ); + const S1 = stategrafik.findMany.data.reduce( + (acc: number, cur: any) => acc + Number(cur.S1 || 0), + 0, + ); setDonutData([ - { name: 'SD', value: SD, color: '#4b6Ef5', key: 'SD' }, - { name: 'SMP', value: SMP, color: '#14b885', key: 'SMP' }, - { name: 'SMA', value: SMA, color: '#E6A03B', key: 'SMA' }, - { name: 'D3', value: D3, color: '#DB524D', key: 'D3' }, - { name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' }, + { name: 'SD', value: SD, color: '#4b6Ef5' }, + { name: 'SMP', value: SMP, color: '#14b885' }, + { name: 'SMA', value: SMA, color: '#E6A03B' }, + { name: 'D3', value: D3, color: '#DB524D' }, + { name: 'S1', value: S1, color: '#1018A8FF' }, ]); } }, [stategrafik.findMany.data]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( @@ -87,21 +111,24 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { return ( + {/* Table Data */} - {/* Header */} - List Pengangguran Berdasarkan Usia Kerja - - - + List Pengangguran Berdasarkan Pendidikan + +
@@ -120,7 +147,9 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
- Belum ada data grafik responden + + Belum ada data grafik responden +
@@ -133,25 +162,30 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { {item.D3} {item.S1} - - - + - - - + )) @@ -161,6 +195,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { + {/* Pagination */}
- {/* Chart */} - - - - Grafik Pengangguran Berdasarkan Pendidikan - {mounted && donutData.length > 0 ? ( - - - - {donutData.map((entry, index) => ( - - ))} - - - - {donutData.map((entry) => ( - - - {entry.name} : {entry.value} - - ))} - - + {/* Donut Chart */} + + + + Grafik Pengangguran Berdasarkan Pendidikan + +
+ {donutData.length > 0 ? ( + ) : ( - Belum ada data untuk ditampilkan dalam grafik + + Belum ada data untuk ditampilkan dalam grafik + )} - - - +
+
+
- {/* Modal Konfirmasi Hapus */} + {/* Modal Hapus */} setModalHapus(false)} onConfirm={handleDelete} - text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?' + text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?" />
); diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx index 8a6ee1b8..ce974a57 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx @@ -2,39 +2,97 @@ 'use client'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core'; +import { + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title +} from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect } from 'react'; -import { useProxy } from 'valtio/utils'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { const router = useRouter(); const params = useParams() as { id: string }; - const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur); + const stategrafik = useProxy( + grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur + ); const id = params.id; + const [isSubmitting, setIsSubmitting] = useState(false); + + // ✅ state lokal, controlled + const [formData, setFormData] = useState({ + usia18_25: '', + usia26_35: '', + usia36_45: '', + usia46_keatas: '', + }); + + const [originalData, setOriginalData] = useState({ + usia18_25: '', + usia26_35: '', + usia36_45: '', + usia46_keatas: '', + }); + + // load data dari global state -> masukin ke local state useEffect(() => { if (id) { stategrafik.findUnique.load(id).then(() => { const data = stategrafik.findUnique.data; if (data) { - stategrafik.update.form = { + setFormData({ usia18_25: data.usia18_25 || '', usia26_35: data.usia26_35 || '', usia36_45: data.usia36_45 || '', usia46_keatas: data.usia46_keatas || '', - }; + }); + setOriginalData({ + usia18_25: data.usia18_25 || '', + usia26_35: data.usia26_35 || '', + usia36_45: data.usia36_45 || '', + usia46_keatas: data.usia46_keatas || '', + }); + } }); } }, [id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + usia18_25: originalData.usia18_25, + usia26_35: originalData.usia26_35, + usia36_45: originalData.usia36_45, + usia46_keatas: originalData.usia46_keatas, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); + // ✅ baru update global state pas submit stategrafik.update.id = id; + stategrafik.update.form = { ...formData }; + await stategrafik.update.submit(); + toast.success('Data grafik berhasil diperbarui!'); router.push( '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia' @@ -42,17 +100,22 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { } catch (error) { console.error(error); toast.error('Terjadi kesalahan saat memperbarui data grafik'); + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Grafik Pengangguran Berdasarkan Usia Kerja @@ -71,44 +134,47 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 18 - 25" type="number" placeholder="Masukkan jumlah" - value={stategrafik.update.form.usia18_25} - onChange={(val) => { - stategrafik.update.form.usia18_25 = val.currentTarget.value; - }} + value={formData.usia18_25} + onChange={(e) => handleChange('usia18_25', e.currentTarget.value)} required /> { - stategrafik.update.form.usia26_35 = val.currentTarget.value; - }} + value={formData.usia26_35} + onChange={(e) => handleChange('usia26_35', e.currentTarget.value)} required /> { - stategrafik.update.form.usia36_45 = val.currentTarget.value; - }} + value={formData.usia36_45} + onChange={(e) => handleChange('usia36_45', e.currentTarget.value)} required /> { - stategrafik.update.form.usia46_keatas = val.currentTarget.value; - }} + value={formData.usia46_keatas} + onChange={(e) => handleChange('usia46_keatas', e.currentTarget.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx index 673256f5..677d488b 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx @@ -2,18 +2,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; -import React, { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { const router = useRouter(); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur); const [donutData, setDonutData] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stategrafik.create.form = { @@ -26,27 +28,33 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { }; const handleSubmit = async () => { - const id = await stategrafik.create.create(); - if (id) { - const idStr = String(id); - await stategrafik.findUnique.load(idStr); - if (stategrafik.findUnique.data) { - setDonutData([stategrafik.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stategrafik.create.create(); + if (id) { + const idStr = String(id); + await stategrafik.findUnique.load(idStr); + if (stategrafik.findUnique.data) { + setDonutData([stategrafik.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'); + } catch (error) { + console.error('Error creating:', error); + toast.error('Terjadi kesalahan saat membuat data'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'); }; return ( {/* Header */} - - - + Tambah Data Pengangguran Berdasarkan Usia @@ -97,6 +105,17 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx index 0c8b70d2..3c0fcfd6 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx @@ -1,13 +1,30 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; +import { + Box, + Button, + Center, + Flex, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { Cell, Pie, PieChart } from 'recharts'; import { useProxy } from 'valtio/utils'; +import { DonutChart } from '@mantine/charts'; import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; @@ -17,8 +34,8 @@ function GrafikBerdasarkanUsiaKerjaYangMenganggur() { return ( } value={search} onChange={(e) => setSearch(e.currentTarget.value)} @@ -31,7 +48,6 @@ function GrafikBerdasarkanUsiaKerjaYangMenganggur() { function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: string }) { const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur); const [donutData, setDonutData] = useState([]); - const [mounted, setMounted] = useState(false); const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); const router = useRouter(); @@ -45,17 +61,10 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri } }; - const { - data, - page, - totalPages, - loading, - load, - } = stategrafik.findMany; + const { data, page, totalPages, loading, load } = stategrafik.findMany; useShallowEffect(() => { - setMounted(true); - load(page, 10, search) + load(page, 10, search); }, [page, search]); useEffect(() => { @@ -64,16 +73,17 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri const totalUsia26_35 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia26_35 || 0), 0); const totalUsia36_45 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia36_45 || 0), 0); const totalUsia46_keatas = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia46_keatas || 0), 0); + setDonutData([ - { name: 'usia18_25', value: totalUsia18_25, color: colors['blue-button'], key: 'usia18_25' }, - { name: 'usia26_35', value: totalUsia26_35, color: '#10A85AFF', key: 'usia26_35' }, - { name: 'usia36_45', value: totalUsia36_45, color: '#C07B13FF', key: 'usia36_45' }, - { name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' }, + { name: 'Usia 18-25', value: totalUsia18_25, color: colors['blue-button'] }, + { name: 'Usia 26-35', value: totalUsia26_35, color: '#10A85AFF' }, + { name: 'Usia 36-45', value: totalUsia36_45, color: '#C07B13FF' }, + { name: 'Usia 46+', value: totalUsia46_keatas, color: '#1094A8FF' }, ]); } }, [stategrafik.findMany.data]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( @@ -85,24 +95,23 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri return ( + {/* Table */} - {/* Header */} List Pengangguran Berdasarkan Usia Kerja - - - + - {/* Table */}
@@ -110,26 +119,38 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri Usia 18-25 Usia 26-35 Usia 36-45 - Usia 46 + + Usia 46+ Edit Delete {filteredData.length > 0 ? ( - filteredData.map(item => ( + filteredData.map((item) => ( {item.usia18_25} {item.usia26_35} {item.usia36_45} {item.usia46_keatas} - - @@ -147,10 +168,10 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
-
+ {/* Pagination */}
- {/* Chart */} + {/* Donut Chart */} - Grafik Pengangguran Berdasarkan Usia Kerja - {mounted && donutData.length > 0 ? ( - - - - {donutData.map((entry, index) => ( - - ))} - - - - - - Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value} - - - - Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value} - - - - - Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value} - - - - - Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value} - - - - + + Grafik Pengangguran Berdasarkan Usia Kerja + + {donutData.length > 0 ? ( +
+ +
) : ( Belum ada data untuk ditampilkan dalam grafik )}
- {/* Modal Konfirmasi Hapus */} { - const total = formData.educatedUnemployment + formData.uneducatedUnemployment; + const [originalData, setOriginalData] = useState({ + month: '', + year: new Date().getFullYear(), + educatedUnemployment: 0, + uneducatedUnemployment: 0, + totalUnemployment: 0, + percentageChange: 0, + }); - let percentageChange = 0; - const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; - const currentMonthIndex = monthOrder.indexOf(formData.month); + // --- hitung total + persentase perubahan + const calculateTotalAndChange = useCallback( + async (data: typeof formData) => { + const total = data.educatedUnemployment + data.uneducatedUnemployment; + let percentageChange = 0; - if (currentMonthIndex !== -1) { - let prevMonthIndex = currentMonthIndex - 1; - let prevYear = formData.year; + const currentMonthIndex = MONTHS.indexOf(data.month); + if (currentMonthIndex !== -1) { + let prevMonthIndex = currentMonthIndex - 1; + let prevYear = data.year; - if (prevMonthIndex < 0) { - prevMonthIndex = 11; - prevYear--; + if (prevMonthIndex < 0) { + prevMonthIndex = 11; + prevYear--; + } + + const prevData = await stateDetail.findByMonthYear.load({ + month: MONTHS[prevMonthIndex], + year: prevYear, + }); + + if (prevData && prevData.totalUnemployment > 0) { + const change = + ((total - prevData.totalUnemployment) / + prevData.totalUnemployment) * 100; + percentageChange = parseFloat(change.toFixed(1)); + } } - const prevMonth = monthOrder[prevMonthIndex]; - const prevData = await stateDetail.findByMonthYear.load({ month: prevMonth, year: prevYear }); - - if (prevData && prevData.totalUnemployment > 0) { - const change = ((total - prevData.totalUnemployment) / prevData.totalUnemployment) * 100; - percentageChange = parseFloat(change.toFixed(1)); - } - } - - return { total, percentageChange }; - }; + return { total, percentageChange }; + }, + [stateDetail.findByMonthYear] + ); + // --- update state lokal const updateFormData = async (updates: Partial) => { const newData = { ...formData, ...updates }; - const { total, percentageChange } = await calculateTotalAndChange(); + const { total, percentageChange } = await calculateTotalAndChange(newData); setFormData({ ...newData, totalUnemployment: total, percentageChange }); }; + // --- load detail by ID (sekali) useEffect(() => { const loadDetail = async () => { const id = params?.id as string; if (!id) return; try { - await stateDetail.findUnique.load(id); // ambil by ID + await stateDetail.findUnique.load(id); const data = stateDetail.findUnique.data; + if (!data) return; - if (data) { - - // Convert year from Date to number if needed - const yearValue = data.year && typeof data.year === 'object' && 'getFullYear' in data.year + const yearValue = + data.year && typeof data.year === 'object' && 'getFullYear' in data.year ? (data.year as Date).getFullYear() : Number(data.year); - // Set the ID for update - stateDetail.update.id = id; + stateDetail.update.id = id; // simpan id untuk update - // Update Valtio state with converted year - stateDetail.update.form = { - ...data, - year: yearValue, - percentageChange: data.percentageChange || 0 // Ensure it's always a number - }; + setFormData({ + month: data.month, + year: yearValue, + educatedUnemployment: data.educatedUnemployment, + uneducatedUnemployment: data.uneducatedUnemployment, + totalUnemployment: data.totalUnemployment, + percentageChange: data.percentageChange || 0, + }); - // Update local formData with converted year - setFormData({ - month: data.month, - year: yearValue, - totalUnemployment: data.totalUnemployment, - educatedUnemployment: data.educatedUnemployment, - uneducatedUnemployment: data.uneducatedUnemployment, - percentageChange: data.percentageChange || 0, // Ensure it's always a number - }); - } - } catch (error) { - console.error("Error loading detail:", error); - toast.error("Gagal memuat data detail"); + setOriginalData({ + month: data.month, + year: yearValue, + educatedUnemployment: data.educatedUnemployment, + uneducatedUnemployment: data.uneducatedUnemployment, + totalUnemployment: data.totalUnemployment, + percentageChange: data.percentageChange || 0, + }); + } catch (err) { + console.error('Error loading detail:', err); + toast.error('Gagal memuat data detail'); } }; loadDetail(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + month: originalData.month, + year: originalData.year, + educatedUnemployment: originalData.educatedUnemployment, + uneducatedUnemployment: originalData.uneducatedUnemployment, + totalUnemployment: originalData.totalUnemployment, + percentageChange: originalData.percentageChange, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + // --- submit form const handleSubmit = async () => { - const { total, percentageChange } = await calculateTotalAndChange(); try { - stateDetail.update.form = { ...formData, totalUnemployment: total, percentageChange }; + setIsSubmitting(true); + const { total, percentageChange } = await calculateTotalAndChange(formData); + + stateDetail.update.form = { + ...formData, + totalUnemployment: total, + percentageChange, + }; + const success = await stateDetail.update.submit(); if (success) { toast.success('Detail data pengangguran berhasil diperbarui!'); router.push('/admin/ekonomi/jumlah-pengangguran'); } - } catch (error) { - console.error('Error updating:', error); + } catch (err) { + console.error('Error updating:', err); toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } }; @@ -129,11 +186,18 @@ function EditDetailDataPengangguran() {
- + ({ - value: p.id, - label: p.namaLengkap, - })) || []} - value={form.atasanId} - onChange={(val) => setForm({ ...form, atasanId: val || '' })} - /> - ({ - value: p.id, - label: p.namaLengkap, - })) || []} - value={form.atasanId} - onChange={(val) => setForm({ ...form, atasanId: val || '' })} - /> - const formatDateForInput = (dateString: string) => { if (!dateString) return ''; const date = new Date(dateString); @@ -64,155 +64,189 @@ export default function EditPegawai() { }; useEffect(() => { - strukturorganisasiState.posisiOrganisasi.findMany.load(); const loadPegawai = async () => { try { + await stateStrukturBumDes.posisiOrganisasi.findManyAll.load(); + const data = await stateOrganisasi.edit.load(id); if (data) { setFormData({ - namaLengkap: data.namaLengkap || "", - gelarAkademik: data.gelarAkademik || "", - imageId: data.imageId || "", - tanggalMasuk: data.tanggalMasuk || "", - email: data.email || "", - telepon: data.telepon || "", - alamat: data.alamat || "", - posisiId: data.posisiId || "", - isActive: data.isActive ?? true, // pakai nullish coalescing + namaLengkap: data.namaLengkap || '', + gelarAkademik: data.gelarAkademik || '', + imageId: data.imageId || '', + tanggalMasuk: data.tanggalMasuk || '', + email: data.email || '', + telepon: data.telepon || '', + alamat: data.alamat || '', + posisiId: data.posisiId || '', + isActive: data.isActive ?? true, + }); + setOriginalData({ + namaLengkap: data.namaLengkap || '', + gelarAkademik: data.gelarAkademik || '', + imageId: data.imageId || '', + tanggalMasuk: data.tanggalMasuk || '', + email: data.email || '', + telepon: data.telepon || '', + alamat: data.alamat || '', + posisiId: data.posisiId || '', + isActive: data.isActive ?? true, + imageUrl: data.image?.link || '', }); - if (data.image?.link) { - setPreviewImage(data.image.link); - } else { - setPreviewImage(null); - } + setPreviewImage(data.image?.link || null); } } catch (error) { - console.error("Error loading pegawai:", error); - toast.error( - error instanceof Error ? error.message : "Gagal mengambil data pegawai" - ); + console.error('Error loading pegawai:', error); + toast.error(error instanceof Error ? error.message : 'Gagal mengambil data pegawai'); } }; loadPegawai(); }, [id]); + const handleResetForm = () => { + setFormData({ + namaLengkap: originalData.namaLengkap, + gelarAkademik: originalData.gelarAkademik, + imageId: originalData.imageId, + tanggalMasuk: originalData.tanggalMasuk, + email: originalData.email, + telepon: originalData.telepon, + alamat: originalData.alamat, + posisiId: originalData.posisiId, + isActive: originalData.isActive, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); if (!formData.namaLengkap.trim()) { - toast.error('Nama lengkap tidak boleh kosong'); - return; + return toast.error('Nama lengkap tidak boleh kosong'); } - stateOrganisasi.edit.form = { - namaLengkap: formData.namaLengkap.trim(), - gelarAkademik: formData.gelarAkademik.trim(), - imageId: formData.imageId ? formData.imageId.trim() : "", - tanggalMasuk: formData.tanggalMasuk.trim(), - email: formData.email.trim(), - telepon: formData.telepon.trim(), - alamat: formData.alamat.trim(), - posisiId: formData.posisiId.trim(), - isActive: formData.isActive, - }; - + // Update global state only on submit + const updatedForm = { ...formData }; - - if (id && !stateOrganisasi.edit.id) { - stateOrganisasi.edit.id = id; + if (file) { + const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); + const uploaded = res.data?.data; + if (!uploaded?.id) return toast.error('Gagal upload gambar'); + updatedForm.imageId = uploaded.id; } + stateOrganisasi.edit.form = updatedForm; + if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id; + const success = await stateOrganisasi.edit.submit(); - - if (success) { - router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); - } + if (success) router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai'); } catch (error) { - console.error("Error updating pegawai:", error); - toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pegawai"); + console.error('Error updating pegawai:', error); + toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai'); + } finally { + setIsSubmitting(false); } - - - }; return ( - - - - + Edit Data Pegawai PPID +
- - - Edit Data Pegawai + + + {/* Nama Lengkap */} setFormData({ ...formData, namaLengkap: e.target.value })} + required /> + + {/* Gelar Akademik */} setFormData({ ...formData, gelarAkademik: e.target.value })} /> - - Gambar - - { - const file = files[0]; // Hanya ambil file pertama - if (file) { - setPreviewImage({ - file, - preview: URL.createObjectURL(file) - }); - } - }} - maxSize={5 * 1024 ** 2} // 5MB - accept={{ - 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] - }} - > - - - - - - - - - - -
- - Drag images here or click to select files - - - Attach as many files as you like, each file should not exceed 5mb - -
-
-
- {previewImage && ( + {/* Foto Profil */} + + Foto Profil + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + Seret gambar atau klik untuk memilih file + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + + + + + {previewImage && ( + Preview - )} - + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )}
+ + {/* Tanggal Masuk */} setFormData({ ...formData, tanggalMasuk: e.target.value })} /> + + {/* Email */} (formData.email = e.currentTarget.value)} + onChange={(e) => setFormData({ ...formData, email: e.target.value })} /> + + {/* Telepon */} (formData.telepon = e.currentTarget.value)} + onChange={(e) => setFormData({ ...formData, telepon: e.target.value })} /> + + {/* Alamat */} (formData.alamat = e.currentTarget.value)} - /> - { - setFormData({ ...formData, isActive: val === 'true' }); - }} + onChange={(e) => setFormData({ ...formData, alamat: e.target.value })} /> + {/* Posisi */} + + Posisi + setFormData({ ...formData, isActive: val === 'true' })} + clearable + /> + + + {/* Submit Button */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */}
-
+
); } diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx index 261a61de..0e0f0dd7 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx @@ -1,42 +1,56 @@ 'use client' import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; -import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; +import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; + import colors from '@/con/colors'; -import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; function DetailPegawai() { - const statePegawai = useProxy(strukturorganisasiState.pegawai) - const [modalHapus, setModalHapus] = useState(false) - const [selectedId, setSelectedId] = useState(null) - const params = useParams() + const statePegawai = useProxy(stateStrukturBumDes.pegawai); + const [modalHapus, setModalHapus] = useState(false); + const [modalNonActive, setModalNonActive] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const params = useParams(); const router = useRouter(); useShallowEffect(() => { - statePegawai.findUnique.load(params?.id as string) - }, []) + stateStrukturBumDes.posisiOrganisasi.findMany.load(); + statePegawai.findUnique.load(params?.id as string); + }, []); const handleHapus = () => { if (selectedId) { - statePegawai.delete.byId(selectedId) - setModalHapus(false) - setSelectedId(null) - router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai") + statePegawai.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); } - } + }; + + const handleNonActive = () => { + if (selectedId) { + statePegawai.nonActive.byId(selectedId); + setModalNonActive(false); + setSelectedId(null); + router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); + } + }; if (!statePegawai.findUnique.data) { return ( - + - ) + ); } + const data = statePegawai.findUnique.data; + return ( @@ -44,91 +58,121 @@ function DetailPegawai() { - - - Detail Pegawai - - + + + + Detail Pegawai PPID + + + + - Nama Lengkap - {statePegawai.findUnique.data?.namaLengkap} - - - Gelar Akademik - {statePegawai.findUnique.data?.gelarAkademik} - - - Image - {statePegawai.findUnique.data?.image?.link ? ( - - ) : ( - Tidak ada gambar - )} - - - Tanggal Masuk - - {statePegawai.findUnique.data?.tanggalMasuk - ? new Date(statePegawai.findUnique.data.tanggalMasuk).toLocaleDateString() - : "-"} + Nama Lengkap + + {data.namaLengkap || '-'} {data.gelarAkademik || ''} - - Email - {statePegawai.findUnique.data?.email} - - - Telepon - {statePegawai.findUnique.data?.telepon} - - - Alamat - {statePegawai.findUnique.data?.alamat} - - - Posisi - - {statePegawai.findUnique.data?.posisi ? ( - - {statePegawai.findUnique.data.posisi.nama} - - ) : ( - - Tidak ada posisi - - )} - - - - Aktif - {statePegawai.findUnique.data?.isActive ? "Ya" : "Tidak"} - - - - - + Posisi + {data.posisi?.nama || '-'} + + + Email + {data.email || '-'} + + + + Telepon + {data.telepon || '-'} + + + + Alamat + {data.alamat || '-'} + + + + Tanggal Masuk + + {data.tanggalMasuk ? new Date(data.tanggalMasuk).toLocaleDateString() : '-'} + + + + + Status + + {data.isActive ? 'Aktif' : 'Tidak Aktif'} + + + + + Foto Profil + {data.image?.link ? ( + {data.namaLengkap + ) : ( + Tidak ada foto profil + )} + + + + + + + + + @@ -139,7 +183,15 @@ function DetailPegawai() { opened={modalHapus} onClose={() => setModalHapus(false)} onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus produk ini?" + text="Apakah Anda yakin ingin menghapus data pegawai ini?" + /> + + {/* Modal NonActive */} + setModalNonActive(false)} + onConfirm={handleNonActive} + text="Apakah Anda yakin ingin menonaktifkan pegawai ini?" /> ); diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx index 540a3daf..c0f86fb0 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx @@ -1,9 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' -import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; + +import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; +import { ActionIcon, Box, Button, Group, Image, Loader, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -11,17 +12,19 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; -function CreatePegawai() { +function CreatePegawaiBumDes() { const router = useRouter(); const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null); - const stateOrganisasi = useProxy(strukturorganisasiState) + const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai) useEffect(() => { - stateOrganisasi.posisiOrganisasi.findMany.load(); + stateStrukturBumDes.posisiOrganisasi.findManyAll.load(); resetForm(); }, []); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { - stateOrganisasi.pegawai.create.form = { + stateOrganisasi.create.form = { namaLengkap: "", gelarAkademik: "", imageId: "", @@ -30,8 +33,10 @@ function CreatePegawai() { telepon: "", alamat: "", posisiId: "", - isActive: true, + isActive: true, }; + setPreviewImage(null); + setFile(null); }; const handleSubmit = async () => { @@ -40,27 +45,30 @@ function CreatePegawai() { } try { + setIsSubmitting(true); // Upload gambar dulu + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } const res = await ApiFetch.api.fileStorage.create.post({ - file: previewImage.file, - name: previewImage.file.name, + file, + name: file.name, }); - const uploaded = res.data?.data; if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); } // Set status aktif secara otomatis - stateOrganisasi.pegawai.create.form.isActive = true; - + stateOrganisasi.create.form.isActive = true; + // Simpan ID gambar ke form - stateOrganisasi.pegawai.create.form.imageId = uploaded.id; - + stateOrganisasi.create.form.imageId = uploaded.id; + // Submit form - await stateOrganisasi.pegawai.create.submit(); - - + await stateOrganisasi.create.submit(); + + // Reset form dan redirect resetForm(); toast.success("Data pegawai berhasil ditambahkan"); @@ -68,134 +76,227 @@ function CreatePegawai() { } catch (error) { console.error("Error creating pegawai:", error); toast.error("Terjadi kesalahan saat menambahkan pegawai"); + } finally { + setIsSubmitting(false); } }; return ( - - - - - - - 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 - )} - + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + +
+ )} +
+ + (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)} + /> + + + + (stateOrganisasi.create.form.email = e.currentTarget.value)} + /> + + + + (stateOrganisasi.create.form.telepon = e.currentTarget.value)} + /> + + + + (stateOrganisasi.create.form.alamat = e.currentTarget.value)} + /> + + + + + Posisi + + ({ - value: p.id, - label: p.nama - })) || []} - value={stateOrganisasi.pegawai.create.form.posisiId} - onChange={(value) => { - if (value) stateOrganisasi.pegawai.create.form.posisiId = value; - }} - searchable - /> - + + + + {/* Tombol Simpan */} + +
); } -export default CreatePegawai; +export default CreatePegawaiBumDes; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx index 2a140fc7..fd17d099 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx @@ -1,33 +1,33 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core'; -import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react'; +import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core'; +import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; -import JudulList from '../../../_com/judulList'; -import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; +import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; -function Pegawai() { + +function PegawaiBumDes() { const [search, setSearch] = useState(""); return ( } value={search} onChange={(e) => setSearch(e.currentTarget.value)} /> - + ); } -function ListPegawai({ search }: { search: string }) { - const stateOrganisasi = useProxy(strukturorganisasiState.pegawai); +function ListPegawaiBumdes({ search }: { search: string }) { + const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai); const router = useRouter(); const { @@ -39,21 +39,10 @@ function ListPegawai({ search }: { search: string }) { } = stateOrganisasi.findMany; useEffect(() => { - load(page, 10); - }, [page]); + load(page, 10, search); + }, [page, search]); - const filteredData = useMemo(() => { - if (!data) return []; - return data.filter(item => { - const keyword = search.toLowerCase(); - return ( - item.namaLengkap?.toLowerCase().includes(keyword) || - item.gelarAkademik?.toLowerCase().includes(keyword) || - item.telepon?.toLowerCase().includes(keyword) || - item.posisi?.nama?.toLowerCase().includes(keyword) - ); - }); - }, [data, search]); + const filteredData = data || [] // Handle loading state if (loading || !data) { @@ -67,29 +56,47 @@ function ListPegawai({ search }: { search: string }) { if (data.length === 0) { return ( - -

Tidak ada data pegawai yang tersedia

+ + + Daftar Pegawai BUMDesa + + +
+ Tidak ada data pegawai yang ditemukan +
); } return ( - - + + + Daftar Pegawai BUMDesa + + - +
- Nama - Gelar Akademik - Telepon - Posisi - Aktif - Detail + Nama Lengkap + Posisi + Status + Aksi @@ -106,10 +113,20 @@ function ListPegawai({ search }: { search: string }) { }) // Aktif di atas ).map((item) => ( - {item.namaLengkap} - {item.gelarAkademik} - {item.telepon} - {item.posisi?.nama} + + + + {item.namaLengkap} + + + + + + + {item.posisi?.nama || 'Belum diatur'} + + + @@ -131,8 +148,15 @@ function ListPegawai({ search }: { search: string }) { - @@ -140,21 +164,22 @@ function ListPegawai({ search }: { search: string }) {
+
+ { + load(newPage, 10); + window.scrollTo(0, 0); + }} + total={totalPages} + withEdges + withControls + radius="md" + /> +
-
- { - load(newPage, 10); - window.scrollTo(0, 0); - }} - total={totalPages} - mt="md" - mb="md" - /> -
); } -export default Pegawai; +export default PegawaiBumDes; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx index 508647a3..a58d838c 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx @@ -1,30 +1,23 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; -import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; +import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; -import { - Box, - Button, - Group, - Paper, - Stack, - Text, - TextInput, - Title, - Tooltip, -} from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; -function EditPosisiOrganisasi() { +function EditPosisiOrganisasiBumDes() { const router = useRouter(); const params = useParams(); const id = params?.id as string; - const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi); + const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', @@ -32,13 +25,23 @@ function EditPosisiOrganisasi() { hierarki: 0, }); - useEffect(() => { - const loadPosisiOrganisasi = async () => { - if (!id) return; + const [originalData, setOriginalData] = useState({ + nama: '', + deskripsi: '', + hierarki: 0, + }); + // Fungsi generik untuk update formData + const handleChange = (field: keyof typeof formData, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + useEffect(() => { + if (!id) return; + + const loadPosisiOrganisasi = async () => { try { const data = await stateOrganisasi.edit.load(id); - if (data) { stateOrganisasi.edit.id = id; setFormData({ @@ -46,9 +49,14 @@ function EditPosisiOrganisasi() { deskripsi: data.deskripsi || '', hierarki: data.hierarki || 0, }); + setOriginalData({ + nama: data.nama || '', + deskripsi: data.deskripsi || '', + hierarki: data.hierarki || 0, + }); } - } catch (error) { - console.error('Error loading posisi organisasi:', error); + } catch (err) { + console.error('Error loading posisi organisasi:', err); toast.error('Gagal memuat data posisi organisasi'); } }; @@ -56,13 +64,24 @@ function EditPosisiOrganisasi() { loadPosisiOrganisasi(); }, [id]); - const handleSubmit = async () => { - try { - if (!formData.nama.trim()) { - toast.error('Nama posisi organisasi tidak boleh kosong'); - return; - } + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + deskripsi: originalData.deskripsi, + hierarki: originalData.hierarki, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { + if (!formData.nama.trim()) { + toast.error('Nama posisi organisasi tidak boleh kosong'); + return; + } + + try { + setIsSubmitting(true); + // Update global state hanya saat submit stateOrganisasi.edit.form = { nama: formData.nama.trim(), deskripsi: formData.deskripsi.trim(), @@ -70,37 +89,30 @@ function EditPosisiOrganisasi() { }; if (!stateOrganisasi.edit.id) { - stateOrganisasi.edit.id = id; // fallback + stateOrganisasi.edit.id = id; } const success = await stateOrganisasi.edit.update(); if (success) { - toast.success('Posisi organisasi berhasil diperbarui!'); - router.push( - '/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi' - ); + router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); } - } catch (error) { - console.error('Error updating posisi organisasi:', error); + } catch (err) { + console.error('Error updating posisi organisasi:', err); + // toast error biasanya sudah ada di update + } finally { + setIsSubmitting(false); } }; return ( - - - + - Edit Posisi Organisasi + Edit Posisi Organisasi BUMDes @@ -114,42 +126,49 @@ function EditPosisiOrganisasi() { > - setFormData({ ...formData, nama: e.target.value }) - } label="Nama Posisi Organisasi" placeholder="Masukkan nama posisi organisasi" + value={formData.nama} + onChange={(e) => handleChange('nama', e.target.value)} required /> - + Deskripsi - setFormData({ ...formData, deskripsi: htmlContent }) - } + onChange={(html) => handleChange('deskripsi', html)} /> - setFormData({ - ...formData, - hierarki: parseInt(e.target.value) || 0, - }) - } label="Hierarki" - placeholder="Masukkan hierarki" + type="number" + min={0} + placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)" + value={formData.hierarki} + onChange={(e) => { + const value = parseInt(e.target.value, 10); + handleChange('hierarki', isNaN(value) ? 0 : value); + }} required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} @@ -169,4 +188,4 @@ function EditPosisiOrganisasi() { ); } -export default EditPosisiOrganisasi; +export default EditPosisiOrganisasiBumDes; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx index ded7678a..1b267c36 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx @@ -1,128 +1,133 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; -import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; +import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; -import { - Box, - Button, - Group, - Paper, - Stack, - Text, - TextInput, - Title, - Tooltip, -} from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; -function CreatePosisiOrganisasi() { - const router = useRouter(); - const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi); +function CreatePosisiOrganisasiBumDes() { + const router = useRouter(); + const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); + const [isSubmitting, setIsSubmitting] = useState(false); - useEffect(() => { - stateOrganisasi.findMany.load(); - }, []); + useEffect(() => { + stateOrganisasi.findMany.load(); + }, []); - const resetForm = () => { - stateOrganisasi.create.form = { - nama: '', - deskripsi: '', - hierarki: 0, - }; + const resetForm = () => { + stateOrganisasi.create.form = { + nama: "", + deskripsi: "", + hierarki: 0, }; + } - const handleSubmit = async () => { - await stateOrganisasi.create.submit(); - resetForm(); - router.push( - '/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi' - ); - }; + const handleSubmit = async () => { + setIsSubmitting(true); + try { + if (!stateOrganisasi.create.form.nama.trim()) { + return toast.error('Nama posisi tidak boleh kosong'); + } - return ( - - {/* Header Back + Title */} - - - - - - Tambah Posisi Organisasi - - + await stateOrganisasi.create.submit(); + toast.success('Posisi organisasi berhasil ditambahkan'); + router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); + } catch (error) { + toast.error('Gagal menambahkan posisi organisasi'); + console.error('Error:', error); + } finally { + setIsSubmitting(false); + } + }; - {/* Form Card */} - + + + + Tambah Posisi Organisasi BUMDes + + + + + + (stateOrganisasi.create.form.nama = e.target.value)} + required + /> + + + + Deskripsi + + { + stateOrganisasi.create.form.deskripsi = htmlContent; + }} + /> + + + { + const value = parseInt(e.target.value, 10); + stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value; + }} + required + /> + + + {/* Tombol Batal */} + - - Deskripsi - { - stateOrganisasi.create.form.deskripsi = htmlContent; - }} - /> - - - { - const value = parseInt(e.currentTarget.value, 10); - if (!isNaN(value)) { - stateOrganisasi.create.form.hierarki = value; - } - }} - required - /> - - {/* Action Button */} - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } -export default CreatePosisiOrganisasi; +export default CreatePosisiOrganisasiBumDes; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx index 33f0cfd0..bf9dc0a2 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx @@ -1,58 +1,37 @@ 'use client' import colors from '@/con/colors'; -import { - Box, - Button, - Center, - Group, - Paper, - Pagination, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip, -} from '@mantine/core'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useShallowEffect } from '@mantine/hooks'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; -import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; +import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; -function PosisiOrganisasi() { + +function PosisiOrganisasiBumDes() { const [search, setSearch] = useState(""); return ( - {/* Search Bar */} } value={search} onChange={(e) => setSearch(e.currentTarget.value)} /> - - {/* List Table */} - + ); } -function ListPosisiOrganisasi({ search }: { search: string }) { - const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi); +function ListPosisiOrganisasiBumDes({ search }: { search: string }) { + const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi) const router = useRouter(); - - const [modalHapus, setModalHapus] = useState(false); - const [selectedId, setSelectedId] = useState(null); + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) const { data, @@ -66,21 +45,20 @@ function ListPosisiOrganisasi({ search }: { search: string }) { load(page, 10, search); }, [page, search]); - const filteredData = data || []; - const handleHapus = async () => { if (selectedId) { await stateOrganisasi.delete.byId(selectedId); - setModalHapus(false); - setSelectedId(null); - load(page, 10, search); // refresh + setModalHapus(false) + setSelectedId(null) } - }; + } + + const filteredData = data || [] if (loading || !data) { return ( - + ); } @@ -89,71 +67,64 @@ function ListPosisiOrganisasi({ search }: { search: string }) { - Daftar Posisi Organisasi - - - + 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 +141,6 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
- - {/* Pagination */}
- {/* Modal Hapus */} setModalHapus(false)} onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus posisi organisasi ini?" + text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?" />
); } -export default PosisiOrganisasi; +export default PosisiOrganisasiBumDes; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx new file mode 100644 index 00000000..89ab5a7e --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx @@ -0,0 +1,131 @@ +/* eslint-disable prefer-const */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import { Box, Center, Image, Loader, Paper, Stack, Text } from '@mantine/core'; +import { IconUsers } from '@tabler/icons-react'; +import { OrganizationChart } from 'primereact/organizationchart'; +import { useEffect } from 'react'; +import { useProxy } from 'valtio/utils'; +import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; + +function StrukturOrganisasiBumDes() { + return ( + + + + ); +} + +function ListStrukturOrganisasiBumDes() { + const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai); + + useEffect(() => { + stateOrganisasi.findMany.load(); + }, []); + + if (stateOrganisasi.findMany.loading) { + return ( +
+ +
+ ); + } + + if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) { + return ( + + + Belum ada struktur organisasi yang ditambahkan + + ); + } + + const posisiMap = new Map(); + + const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive); + + for (const pegawai of aktifPegawai) { + const posisiId = pegawai.posisi.id; + if (!posisiMap.has(posisiId)) { + posisiMap.set(posisiId, { + ...pegawai.posisi, + pegawaiList: [], + children: [], + }); + } + posisiMap.get(posisiId)!.pegawaiList.push(pegawai); + } + + let root: any[] = []; + posisiMap.forEach((posisi) => { + if (posisi.parentId) { + const parent = posisiMap.get(posisi.parentId); + if (parent) { + parent.children.push(posisi); + } + } else { + root.push(posisi); + } + }); + + function toOrgChartFormat(node: any): any { + return { + expanded: true, + type: 'person', + styleClass: 'p-person', + data: { + name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ada pegawai', + status: node.nama, + image: node.pegawaiList?.[0]?.image?.link || '/img/default.png', + }, + children: node.children.map(toOrgChartFormat), + }; + } + + const chartData = root.map(toOrgChartFormat); + + return ( + + + + + + ); +} + +function nodeTemplate(node: any) { + const imageSrc = node?.data?.image || '/img/default.png'; + const name = node?.data?.name || 'Tanpa Nama'; + const status = node?.data?.status || 'Tidak ada deskripsi'; + + return ( + + {name} + {name} + {status} + + ); +} + +export default StrukturOrganisasiBumDes; diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx index c00259b1..51a1ab7f 100644 --- a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -67,21 +67,19 @@ function DetailAjukanIdeInofativDesa() { Detail Ajukan Ide Inovatif Desa - - - + {/* Detail Data */} @@ -94,7 +92,7 @@ function DetailAjukanIdeInofativDesa() { Alamat - + @@ -104,12 +102,12 @@ function DetailAjukanIdeInofativDesa() { Deskripsi - + Masalah - {data?.masalah || '-'} + {data?.masalah || '-'} diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx index 51ba6814..24bdbd06 100644 --- a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx @@ -4,7 +4,6 @@ import { Box, Button, Center, - Group, Pagination, Paper, Skeleton, @@ -16,11 +15,10 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconDeviceImac, IconSearch, IconPlus } from '@tabler/icons-react'; +import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -72,20 +70,7 @@ function ListAjukanIdeInovatif({ search }: { search: string }) { return ( - - Daftar Ide Inovatif - - - - - + Daftar Ide Inovatif diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx index 715e3c0d..f22190da 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx @@ -1,10 +1,22 @@ -'use client' +'use client'; /* eslint-disable react-hooks/exhaustive-deps */ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { + ActionIcon, + Box, + Button, + Group, + Image, + Loader, + Paper, + Stack, + Text, + TextInput, + Title +} from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -12,20 +24,28 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; -function EditPenghargaan() { - const stateDesaDigital = useProxy(desaDigitalState) - const router = useRouter() - const params = useParams() - const [previewImage, setPreviewImage] = useState(null) - const [file, setFile] = useState(null) +function EditDigitalSmartVillage() { + const stateDesaDigital = useProxy(desaDigitalState); + const router = useRouter(); + const params = useParams(); + + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - name: stateDesaDigital.findUnique.data?.name || '', - deskripsi: stateDesaDigital.findUnique.data?.deskripsi || '', - imageId: stateDesaDigital.findUnique.data?.imageId || '', - }) + name: '', + deskripsi: '', + imageId: '', + }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + imageUrl: '', + }); useEffect(() => { - const loadPenghargaan = async () => { + const loadData = async () => { const id = params?.id as string; if (!id) return; @@ -37,137 +57,210 @@ function EditPenghargaan() { deskripsi: data.deskripsi || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + if (data?.image?.link) setPreviewImage(data.image.link); } } catch (error) { - console.error("Error loading desa digital smart village:", error); - toast.error("Gagal memuat data desa digital smart village"); + console.error('Error loading data:', error); + toast.error('Gagal memuat data desa digital smart village'); } }; - loadPenghargaan(); + loadData(); }, [params?.id]); const handleSubmit = async () => { try { - stateDesaDigital.edit.form = { - ...stateDesaDigital.edit.form, - name: formData.name, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - } + setIsSubmitting(true); + stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData }; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } + if (!uploaded?.id) return toast.error('Gagal upload gambar'); stateDesaDigital.edit.form.imageId = uploaded.id; } await stateDesaDigital.edit.update(); - toast.success("Desa digital smart village berhasil diperbarui!"); - router.push("/admin/inovasi/desa-digital-smart-village"); + toast.success('Desa digital smart village berhasil diperbarui!'); + router.push('/admin/inovasi/desa-digital-smart-village'); } catch (error) { - console.error("Error updating desa digital smart village:", error); - toast.error("Terjadi kesalahan saat memperbarui desa digital smart village"); + console.error('Error updating desa digital:', error); + toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } - } + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info('Form dikembalikan ke data awal'); + }; return ( - - - - - - - Edit Desa Digital Smart Village + + Edit Desa Digital Smart Village + + + + {/* Form Card */} + + + {/* Dropzone Upload */} + + + Gambar Desa Digital + + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + + + + + + {previewImage && ( + + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + + + {/* Input Judul */} setFormData({ ...formData, name: e.target.value })} - label={Judul} - placeholder="masukkan judul" + required /> - - Gambar - - { - const selectedFile = files[0]; // Ambil file pertama - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview - } - }} - onReject={() => toast.error('File tidak valid.')} - maxSize={5 * 1024 ** 2} // Maks 5MB - accept={{ 'image/*': [] }} - > - - - - - - - - - - -
- - Drag gambar ke sini atau klik untuk pilih file - - - Maksimal 5MB dan harus format gambar - -
-
-
- - {/* Tampilkan preview kalau ada */} - {previewImage && ( - - - - )} -
-
+ {/* Editor Deskripsi */} - Deskripsi + + Deskripsi + { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - stateDesaDigital.edit.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> - + {/* Tombol Simpan */} + + + + {/* 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..236f2b17 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx @@ -1,8 +1,17 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { + Box, + Button, + Group, + Image, + Paper, + Skeleton, + Stack, + Text +} from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -10,95 +19,132 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import desaDigitalState from '../../../_state/inovasi/desa-digital'; function DetailDesaDigital() { - const stateDesaDigital = useProxy(desaDigitalState) + const stateDesaDigital = useProxy(desaDigitalState); const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); - const router = useRouter() - const params = useParams() + const router = useRouter(); + const params = useParams(); useShallowEffect(() => { - stateDesaDigital.findUnique.load(params?.id as string) - }, []) + stateDesaDigital.findUnique.load(params?.id as string); + }, []); const handleHapus = () => { if (selectedId) { - stateDesaDigital.delete.byId(selectedId) - setModalHapus(false) - setSelectedId(null) - router.push("/admin/inovasi/desa-digital-smart-village") + stateDesaDigital.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push("/admin/inovasi/desa-digital-smart-village"); } - } + }; if (!stateDesaDigital.findUnique.data) { return ( - + - ) + ); } + const data = stateDesaDigital.findUnique.data; + return ( - - - - - - - Detail Desa Digital Smart Village - {stateDesaDigital.findUnique.data ? ( - - - - Judul - {stateDesaDigital.findUnique.data?.name} - - - Deskripsi - - - - Gambar - - - - - - - - - ) : null} + + {/* 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 */} + + + + + + + - {/* Modal Konfirmasi Hapus */} + {/* Modal Konfirmasi */} setModalHapus(false)} onConfirm={handleHapus} - text='Apakah anda yakin ingin menghapus desa digital smart village ini?' + text="Apakah Anda yakin ingin menghapus desa digital smart village ini?" /> ); diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx index 289651f0..74e1bd82 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx @@ -2,31 +2,33 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, - Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import ExifOrientationImg from 'react-exif-orientation-img'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import desaDigitalState from '../../../_state/inovasi/desa-digital'; -import { Dropzone } from '@mantine/dropzone'; -function CreateDesaDigital() { +export default function CreateDesaDigital() { const stateDesaDigital = useProxy(desaDigitalState); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateDesaDigital.create.form = { @@ -44,7 +46,7 @@ function CreateDesaDigital() { } try { - // Upload gambar dulu + setIsSubmitting(true); const uploadRes = await ApiFetch.api.fileStorage.create.post({ file, name: file.name, @@ -55,10 +57,8 @@ function CreateDesaDigital() { return toast.error('Gagal mengunggah gambar'); } - // Set imageId ke form stateDesaDigital.create.form.imageId = uploaded.id; - // Submit form const success = await stateDesaDigital.create.create(); if (success) { resetForm(); @@ -67,45 +67,55 @@ function CreateDesaDigital() { } catch (error) { console.error('Error in handleSubmit:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; return ( - {/* Header */} - - - - + {/* Header dengan tombol kembali */} + + Tambah Desa Digital Smart Village - {/* Card */} + {/* Card Form */} - - {/* Nama */} + + {/* Input Nama */} (stateDesaDigital.create.form.name = e.target.value)} - required + radius="md" + withAsterisk /> {/* Deskripsi */} - + Deskripsi - - Gambar + + Gambar Desa Digital { @@ -131,9 +141,14 @@ function CreateDesaDigital() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" + style={{ + border: '2px dashed #cfd8dc', + backgroundColor: '#fafafa', + transition: 'background-color 0.2s ease, border-color 0.2s ease', + }} > @@ -153,24 +168,61 @@ function CreateDesaDigital() { {/* Preview */} {previewImage && ( - - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Tombol Submit */} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} @@ -189,5 +241,3 @@ function CreateDesaDigital() { ); } - -export default CreateDesaDigital; diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx index f1c1f484..72d12073 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -68,18 +67,16 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) { List Desa Digital Smart Village - - - +
@@ -97,17 +94,21 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) { filteredData.map((item) => ( - - {item.name} - + + + {item.name} + + - + + + - + Edit Info Teknologi Tepat Guna @@ -120,7 +149,7 @@ function EditInfoTeknologiTepatGuna() { label="Judul" placeholder="Masukkan judul info teknologi tepat guna" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))} required /> @@ -139,7 +168,7 @@ function EditInfoTeknologiTepatGuna() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -158,25 +187,45 @@ function EditInfoTeknologiTepatGuna() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -188,15 +237,25 @@ function EditInfoTeknologiTepatGuna() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - stateInfoTekno.edit.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> {/* Tombol submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx index 4313fcaa..1ddee92a 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -54,7 +54,7 @@ function DetailInfoTeknologiTepatGuna() { {/* Card Utama */} - + Judul @@ -78,6 +78,7 @@ function DetailInfoTeknologiTepatGuna() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -100,35 +101,31 @@ function DetailInfoTeknologiTepatGuna() { {/* Action Buttons */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx index d2d09028..644e1e8d 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -20,13 +22,13 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import infoTeknoState from '../../../_state/inovasi/info-tekno'; -import { Dropzone } from '@mantine/dropzone'; function CreateInfoTeknologiTepatGuna() { const stateInfoTekno = useProxy(infoTeknoState); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateInfoTekno.create.form = { @@ -44,6 +46,7 @@ function CreateInfoTeknologiTepatGuna() { } try { + setIsSubmitting(true); const uploadRes = await ApiFetch.api.fileStorage.create.post({ file: file, name: file.name, @@ -65,6 +68,8 @@ function CreateInfoTeknologiTepatGuna() { } catch (error) { console.error('Error in handleSubmit:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; @@ -72,11 +77,9 @@ function CreateInfoTeknologiTepatGuna() { {/* Header */} - - - + Tambah Info Teknologi Tepat Guna @@ -131,7 +134,7 @@ function CreateInfoTeknologiTepatGuna() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -152,7 +155,7 @@ function CreateInfoTeknologiTepatGuna() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx index 3fcb8f8a..fe766900 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx @@ -4,6 +4,7 @@ import { Box, Button, Center, + Group, Pagination, Paper, Skeleton, @@ -15,9 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Group, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -68,18 +67,16 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) { Daftar Info Teknologi Tepat Guna - - - + diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx index 781f1ec5..c208cdac 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { IconListDetails, IconUsers } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconListDetails, IconUsers } from '@tabler/icons-react'; function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -15,14 +15,12 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { label: "List Kolaborasi Inovasi", value: "listkolaborasiinovasi", href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi", - tooltip: "Lihat daftar kolaborasi inovasi", icon: , }, { label: "Mitra Kolaborasi", value: "mitarakolaborasi", href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi", - tooltip: "Kelola mitra kolaborasi", icon: , } ]; @@ -73,25 +71,18 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx index 24827165..531aca12 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx @@ -8,13 +8,14 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from "@mantine/core"; +import { YearPickerInput } from "@mantine/dates"; import { IconArrowBack } from "@tabler/icons-react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -25,16 +26,25 @@ function EditKolaborasiInovasi() { const kolaborasiState = useProxy(kolaborasiInovasiState); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - name: kolaborasiState.update.form.name || "", - deskripsi: kolaborasiState.update.form.deskripsi || "", - tahun: kolaborasiState.update.form.tahun || "", - slug: kolaborasiState.update.form.slug || "", - kolaborator: kolaborasiState.update.form.kolaborator || "", + name: "", + deskripsi: "", + tahun: "", + slug: "", + kolaborator: "", }); - // Load data + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + tahun: "", + slug: "", + kolaborator: "", + }); + + // Load data awal dari server useEffect(() => { const loadKolaborasi = async () => { const id = params?.id as string; @@ -44,12 +54,19 @@ function EditKolaborasiInovasi() { const data = await kolaborasiState.update.load(id); if (data) { setFormData({ - name: data.name || "", - deskripsi: data.deskripsi || "", - tahun: data.tahun || "", - slug: data.slug || "", - kolaborator: data.kolaborator || "", + name: data.name ?? "", + deskripsi: data.deskripsi ?? "", + tahun: data.tahun?.toString() ?? "", + slug: data.slug ?? "", + kolaborator: data.kolaborator ?? "", }); + setOriginalData({ + name: data.name ?? "", + deskripsi: data.deskripsi ?? "", + tahun: data.tahun?.toString() ?? "", + slug: data.slug ?? "", + kolaborator: data.kolaborator ?? "", + }) } } catch (error) { console.error("Error loading kolaborasi:", error); @@ -60,8 +77,21 @@ function EditKolaborasiInovasi() { loadKolaborasi(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + tahun: originalData.tahun?.toString(), + slug: originalData.slug, + kolaborator: originalData.kolaborator, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + // Handler submit → baru update global state const handleSubmit = async () => { try { + setIsSubmitting(true); kolaborasiState.update.form = { ...kolaborasiState.update.form, name: formData.name, @@ -70,23 +100,29 @@ function EditKolaborasiInovasi() { slug: formData.slug, kolaborator: formData.kolaborator, }; + await kolaborasiState.update.submit(); toast.success("Kolaborasi inovasi berhasil diperbarui!"); - router.push("/admin/inovasi/kolaborasi-inovasi"); + router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"); } catch (error) { console.error("Error updating kolaborasi:", error); toast.error("Terjadi kesalahan saat memperbarui data"); + } finally { + setIsSubmitting(false); } }; + // Handler input (biar lebih DRY) + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + return ( - - - + Edit Kolaborasi Inovasi @@ -105,7 +141,7 @@ function EditKolaborasiInovasi() { label="Nama Kolaborasi" placeholder="Masukkan nama kolaborasi" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange("name", e.target.value)} required /> @@ -113,23 +149,26 @@ function EditKolaborasiInovasi() { label="Deskripsi Singkat" placeholder="Masukkan deskripsi singkat" value={formData.slug} - onChange={(e) => setFormData({ ...formData, slug: e.target.value })} + onChange={(e) => handleChange("slug", e.target.value)} required /> - setFormData({ ...formData, tahun: e.target.value })} - required + Tahun} + placeholder="Pilih tahun" + onChange={(date) => { + const year = date ? new Date(date).getFullYear() : 0; + handleChange("tahun", year.toString()); + }} /> setFormData({ ...formData, kolaborator: e.target.value })} + onChange={(e) => handleChange("kolaborator", e.target.value)} required /> @@ -139,25 +178,34 @@ function EditKolaborasiInovasi() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - kolaborasiState.update.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => handleChange("deskripsi", htmlContent)} /> + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx index cc4d06f5..577f963f 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -81,12 +81,12 @@ function DetailKolaborasiInovasi() { Deskripsi Singkat - {data?.slug || '-'} + {data?.slug || '-'} Deskripsi - + @@ -96,33 +96,29 @@ function DetailKolaborasiInovasi() { {/* Tombol aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx index a45d50ad..deca2518 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx @@ -3,17 +3,18 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { YearPickerInput } from '@mantine/dates'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateProgramKreatifDesa() { const stateCreate = useProxy(kolaborasiInovasiState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateCreate.create.form = { @@ -40,6 +41,7 @@ function CreateProgramKreatifDesa() { const handleSubmit = async () => { try { + setIsSubmitting(true); await stateCreate.create.create(); resetForm(); router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"); @@ -47,6 +49,8 @@ function CreateProgramKreatifDesa() { } catch (error) { console.error("Error creating kolaborasi inovasi:", error); toast.error("Terjadi kesalahan saat menyimpan data"); + } finally { + setIsSubmitting(false); } }; @@ -54,11 +58,9 @@ function CreateProgramKreatifDesa() { {/* Back Button */} - - - + Tambah Kolaborasi Inovasi @@ -111,6 +113,17 @@ function CreateProgramKreatifDesa() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx index f0899d36..576ea0c4 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx @@ -17,15 +17,14 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; -import HeaderSearch from '../../../_com/header'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; function KolaborasiInovasi() { const [search, setSearch] = useState(''); @@ -68,16 +67,14 @@ function ListKolaborasiInovasi({ search }: { search: string }) { Daftar Kolaborasi Inovasi - - - +
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx index ac9c1ea2..ce4516a7 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx @@ -4,17 +4,18 @@ import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolabo import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Center, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { @@ -33,17 +34,29 @@ function EditMitraKolaborasi() { const state = useProxy(mitraKolaborasi); const router = useRouter(); const params = useParams(); + const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Local form state (controlled) const [formData, setFormData] = useState({ - name: state.update.form.name || '', - imageId: state.update.form.imageId || '', + name: '', + imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + imageId: '', + imageUrl: '', + }); + + // Load data ke state lokal sekali saja useEffect(() => { - const loadFoto = async () => { + const loadData = async () => { const id = params?.id as string; if (!id) return; + try { const data = await state.update.load(id); if (data) { @@ -51,25 +64,47 @@ function EditMitraKolaborasi() { name: data.name || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); + if (data?.image?.link) { setPreviewImage(data.image.link); } } } catch (error) { - console.error('Error loading foto:', error); - toast.error('Gagal memuat data foto'); + console.error('Error loading data:', error); + toast.error('Gagal memuat data mitra'); } }; - loadFoto(); + + loadData(); }, [params?.id]); + const handleChange = (key: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [key]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { - state.update.form = { - ...state.update.form, - name: formData.name, - imageId: formData.imageId, - }; + setIsSubmitting(true); + // upload file jika ada + let imageId = formData.imageId; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ file, @@ -79,14 +114,24 @@ function EditMitraKolaborasi() { if (!uploaded?.id) { return toast.error('Gagal upload gambar'); } - state.update.form.imageId = uploaded.id; + imageId = uploaded.id; } + + // update global state hanya saat submit + state.update.form = { + ...state.update.form, + name: formData.name, + imageId, + }; + await state.update.update(); toast.success('Mitra berhasil diperbarui!'); router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi'); } catch (error) { console.error('Error updating mitra:', error); toast.error('Terjadi kesalahan saat memperbarui mitra'); + } finally { + setIsSubmitting(false); } }; @@ -94,11 +139,9 @@ function EditMitraKolaborasi() { {/* Header */} - - - + Edit Mitra @@ -119,7 +162,7 @@ function EditMitraKolaborasi() { label="Nama Mitra" placeholder="Masukkan nama mitra" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange('name', e.target.value)} required /> @@ -138,7 +181,7 @@ function EditMitraKolaborasi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -157,7 +200,7 @@ function EditMitraKolaborasi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp @@ -165,7 +208,7 @@ function EditMitraKolaborasi() { {/* Preview Foto */} {previewImage ? ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : (
@@ -187,6 +248,17 @@ function EditMitraKolaborasi() { {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx index 2c9a077c..195c1ab2 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx @@ -3,16 +3,17 @@ import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolabo import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -26,6 +27,7 @@ function CreateMitraKolaborasi() { const router = useRouter(); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { state.create.form = { @@ -37,35 +39,41 @@ function CreateMitraKolaborasi() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + state.create.form.imageId = uploaded.id; + await state.create.create(); + resetForm(); + router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi'); + } catch (error) { + console.error("Error creating mitra kolaborasi:", error); + toast.error("Terjadi kesalahan saat menambahkan mitra kolaborasi"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - state.create.form.imageId = uploaded.id; - await state.create.create(); - resetForm(); - router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi'); }; return ( {/* Back Button + Title */} - - - + Tambah Mitra Kolaborasi @@ -105,7 +113,7 @@ function CreateMitraKolaborasi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -126,7 +134,7 @@ function CreateMitraKolaborasi() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx index a2d6cbbb..9fde4fb1 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx @@ -18,16 +18,15 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { IconEdit, IconSearch, IconX, IconPlus } from '@tabler/icons-react'; +import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; -import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi'; function MitraKolaborasi() { const [search, setSearch] = useState(''); @@ -81,20 +80,18 @@ function ListMitraKolaborasi({ search }: { search: string }) { Daftar Mitra Kolaborasi - - - +
@@ -135,39 +132,35 @@ function ListMitraKolaborasi({ search }: { search: string }) { - - - + - - - + )) diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx index 55a8cdb2..b12c50be 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx @@ -8,17 +8,16 @@ import { TabsList, TabsPanel, TabsTab, - Title, - Tooltip + Title } from '@mantine/core'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; import { + IconAlertCircle, IconFileText, IconListDetails, - IconMessage, - IconAlertCircle + IconMessage } from '@tabler/icons-react'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -30,29 +29,25 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode } label: "Administrasi Online", value: "administrasionline", href: "/admin/inovasi/layanan-online-desa/administrasi-online", - icon: , - tooltip: "Kelola administrasi online desa" + icon: }, { label: "Jenis Layanan", value: "jenislayanan", href: "/admin/inovasi/layanan-online-desa/jenis-layanan", - icon: , - tooltip: "Daftar jenis layanan desa" + icon: }, { label: "Pengaduan Masyarakat", value: "pengaduanmasyarakat", href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat", - icon: , - tooltip: "Laporan pengaduan masyarakat" + icon: }, { label: "Jenis Pengaduan", value: "jenispengaduan", href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan", - icon: , - tooltip: "Kategori/jenis pengaduan masyarakat" + icon: } ]; @@ -103,25 +98,18 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode } }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx index b08d0582..72f2ddd8 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -53,20 +53,18 @@ function DetailAdministrasiOnline() { Kembali - - - + {/* Konten Detail */} { const loadJenisLayanan = async () => { const id = params?.id as string; if (!id) return; + try { const data = await state.edit.load(id); if (data) { setFormData({ - nama: data.nama, - deskripsi: data.deskripsi, + nama: data.nama ?? '', + deskripsi: data.deskripsi ?? '', + }); + setOriginalData({ + nama: data.nama ?? '', + deskripsi: data.deskripsi ?? '', }); } } catch (error) { - console.error("Error loading jenis layanan:", error); - toast.error("Gagal memuat data jenis layanan"); + console.error('Error loading jenis layanan:', error); + toast.error('Gagal memuat data jenis layanan'); } }; + loadJenisLayanan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + deskripsi: originalData.deskripsi, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); state.edit.form = { ...state.edit.form, - nama: formData.nama, - deskripsi: formData.deskripsi, - } - await state.edit.update() - toast.success("Jenis layanan berhasil diperbarui!") - router.push("/admin/inovasi/layanan-online-desa/jenis-layanan") + ...formData, + }; + + await state.edit.update(); + toast.success('Jenis layanan berhasil diperbarui!'); + router.push('/admin/inovasi/layanan-online-desa/jenis-layanan'); } catch (error) { - console.error("Error updating jenis layanan:", error); - toast.error("Terjadi kesalahan saat memperbarui jenis layanan"); + console.error('Error updating jenis layanan:', error); + toast.error('Terjadi kesalahan saat memperbarui jenis layanan'); + } finally { + setIsSubmitting(false); } - } + }; return ( - - - - - - - Edit Jenis Layanan + + Edit Jenis Layanan + + + + {/* Form Container */} + + + {/* Input: Nama Jenis Layanan */} { - setFormData({ ...formData, nama: val.target.value }); - }} - label={Nama Jenis Layanan} - placeholder="masukkan nama jenis layanan" + onChange={(e) => + setFormData((prev) => ({ ...prev, nama: e.target.value })) + } + required /> + + {/* Input: Deskripsi (Rich Text Editor) */} - Deskripsi + + Deskripsi + { - setFormData({ ...formData, deskripsi: htmlContent }); - }} + onChange={(htmlContent) => + setFormData((prev) => ({ + ...prev, + deskripsi: htmlContent, + })) + } /> - + + {/* Tombol Submit */} + + + + {/* Tombol Simpan */} + + diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx index 427aaada..babc24f3 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -79,38 +79,35 @@ function DetailJenisLayanan() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> {/* Tombol aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx index 50d76bc1..4dc93703 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx @@ -6,21 +6,23 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJenisLayanan() { const router = useRouter(); const statePasar = useProxy(layananonlineDesa.jenisLayanan); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { statePasar.findMany.load(); @@ -34,25 +36,31 @@ function CreateJenisLayanan() { }; const handleSubmit = async () => { - await statePasar.create.create(); - resetForm(); - router.push('/admin/inovasi/layanan-online-desa/jenis-layanan'); + try { + setIsSubmitting(true); + await statePasar.create.create(); + resetForm(); + router.push('/admin/inovasi/layanan-online-desa/jenis-layanan'); + } catch (error) { + console.error('Error creating jenis layanan:', error); + toast.error('Terjadi kesalahan saat menambahkan jenis layanan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header dengan tombol back */} - - - + Tambah Jenis Layanan @@ -88,6 +96,17 @@ function CreateJenisLayanan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx index ba176cd6..a1ea1da1 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -68,20 +67,18 @@ function ListJenisLayanan({ search }: { search: string }) { Daftar Jenis Layanan - - - +
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx index 6444952c..5331bc9e 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -23,11 +23,17 @@ function EditJenisPengaduan() { const params = useParams(); const id = params?.id as string; const state = useProxy(layananonlineDesa.jenisPengaduan); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - nama: "", + nama: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + }); + + // Load data sekali aja useEffect(() => { const loadJenisPengaduan = async () => { if (!id) return; @@ -36,68 +42,86 @@ function EditJenisPengaduan() { const data = await state.edit.load(id); if (data) { - // pastikan id-nya masuk ke state edit - state.edit.id = id; + state.edit.id = id; // inject id ke state global (hanya sekali) setFormData({ nama: data.nama || '', }); + setOriginalData({ + nama: data.nama || '', + }); } } catch (error) { - console.error("Error loading jenis pengaduan:", error); - toast.error("Gagal memuat data jenis pengaduan"); + console.error('Error loading jenis pengaduan:', error); + toast.error('Gagal memuat data jenis pengaduan'); } }; loadJenisPengaduan(); }, [id]); + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { + if (!formData.nama.trim()) { + toast.error('Nama jenis pengaduan tidak boleh kosong'); + return; + } + + // Update ke global state HANYA pas submit + state.edit.form = { + nama: formData.nama.trim(), + }; + + // Safety fallback kalau ID belum ada + if (!state.edit.id) { + state.edit.id = id; + } + try { - if (!formData.nama.trim()) { - toast.error('Nama jenis pengaduan tidak boleh kosong'); - return; - } - - state.edit.form = { - nama: formData.nama.trim(), - }; - - // Safety check tambahan: pastikan ID tidak kosong - if (!state.edit.id) { - state.edit.id = id; // fallback - } - + setIsSubmitting(true); const success = await state.edit.update(); if (success) { - router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan"); + router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan'); } } catch (error) { - console.error("Error updating jenis pengaduan:", error); - // toast akan ditampilkan dari fungsi update + console.error('Error updating jenis pengaduan:', error); + // toast ditangani di dalam state.update + } finally { + setIsSubmitting(false); } }; return ( - {/* Header + tombol back */} + {/* Header */} - - - + Edit Jenis Pengaduan - {/* Card Form */} + {/* Form */} setFormData({ ...formData, nama: e.target.value })} + onChange={(e) => handleChange('nama', e.target.value)} label="Nama Jenis Pengaduan" placeholder="Masukkan nama jenis pengaduan" required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx index e3175ede..9167a525 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx @@ -6,21 +6,22 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJenisPengaduan() { const router = useRouter(); const state = useProxy(layananonlineDesa.jenisPengaduan); - + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { state.findMany.load(); }, []); @@ -32,20 +33,26 @@ function CreateJenisPengaduan() { }; const handleSubmit = async () => { - await state.create.create(); - resetForm(); - router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan'); + try { + setIsSubmitting(true); + await state.create.create(); + resetForm(); + router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan'); + } catch (error) { + console.error('Error creating jenis pengaduan:', error); + toast.error('Terjadi kesalahan saat menambahkan jenis pengaduan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Jenis Pengaduan @@ -70,6 +77,17 @@ function CreateJenisPengaduan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx index 4bbee3b6..ecab5a98 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; @@ -79,20 +78,18 @@ function ListJenisPengaduan({ search }: { search: string }) { Daftar Jenis Pengaduan - - - + diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx index abcbb91f..9ab1109e 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; @@ -102,7 +102,7 @@ function DetailPengaduanMasyarakat() { Deskripsi Pengaduan - + @@ -129,21 +129,19 @@ function DetailPengaduanMasyarakat() { {/* Action Button */} - - - + diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx index 11140fff..429bc44f 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; @@ -99,18 +98,16 @@ function ListPengaduanMasyarakat({ search }: { search: string }) { {item.nomorTelepon} - - - + )) diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx index fc9b335f..294d25b0 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx @@ -1,18 +1,19 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap'; import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -20,7 +21,6 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import SelectIconProgramEdit from '../../../../_com/selectIconEdit'; -import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap'; interface FormProgramKreatif { name: string; @@ -33,6 +33,7 @@ function EditProgramKreatifDesa() { const stateProgramKreatif = useProxy(programKreatifState); const params = useParams(); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -41,6 +42,16 @@ function EditProgramKreatifDesa() { icon: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + slug: '', + icon: '', + }); + + const [isDataChanged, setIsDataChanged] = useState(false); + + // Load data hanya sekali berdasarkan params.id useEffect(() => { const loadProgramKreatif = async () => { const id = params?.id as string; @@ -50,18 +61,14 @@ function EditProgramKreatifDesa() { const data = await stateProgramKreatif.update.load(id); if (data) { stateProgramKreatif.update.id = id; - stateProgramKreatif.update.form = { - name: data.name, - slug: data.slug, - deskripsi: data.deskripsi, - icon: data.icon, + const loadedData = { + name: data.name || '', + slug: data.slug || '', + deskripsi: data.deskripsi || '', + icon: data.icon || '', }; - setFormData({ - name: data.name, - slug: data.slug, - deskripsi: data.deskripsi, - icon: data.icon, - }); + setFormData(loadedData); + setOriginalData(loadedData); } } catch (error) { console.error('Error loading program kreatif:', error); @@ -72,31 +79,97 @@ function EditProgramKreatifDesa() { loadProgramKreatif(); }, [params?.id]); + // Deteksi perubahan data + useEffect(() => { + const hasChanged = + formData.name !== originalData.name || + formData.slug !== originalData.slug || + formData.deskripsi !== originalData.deskripsi || + formData.icon !== originalData.icon; + + setIsDataChanged(hasChanged); + }, [formData, originalData]); + + // Prevent browser back/refresh jika ada perubahan + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isDataChanged) { + e.preventDefault(); + e.returnValue = ''; + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [isDataChanged]); + + const handleChange = + (field: keyof FormProgramKreatif) => + (value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleBackClick = () => { + if (isDataChanged) { + const confirmed = window.confirm( + 'Anda memiliki perubahan yang belum disimpan. Apakah Anda yakin ingin keluar dari halaman ini? Semua perubahan akan hilang.' + ); + if (confirmed) { + router.back(); + } + } else { + router.back(); + } + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + slug: originalData.slug, + deskripsi: originalData.deskripsi, + icon: originalData.icon, + }); + setOriginalData({ + ...originalData, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateProgramKreatif.update.form = { - ...stateProgramKreatif.update.form, name: formData.name.trim(), deskripsi: formData.deskripsi.trim(), slug: formData.slug.trim(), icon: formData.icon.trim(), }; await stateProgramKreatif.update.submit(); + + // Reset isDataChanged agar tidak muncul konfirmasi setelah save + setOriginalData(formData); + setIsDataChanged(false); + router.push('/admin/inovasi/program-kreatif-desa'); } catch (error) { console.error('Error updating program kreatif:', error); - toast.error('Gagal memuat data program kreatif'); + toast.error('Gagal menyimpan program kreatif'); + } finally { + setIsSubmitting(false); } }; return ( - - - + Edit Program Kreatif Desa @@ -115,7 +188,7 @@ function EditProgramKreatifDesa() { label="Nama Program Kreatif Desa" placeholder="Masukkan nama program kreatif desa" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange('name')(e.target.value)} required /> @@ -123,7 +196,7 @@ function EditProgramKreatifDesa() { label="Deskripsi Singkat Program Kreatif Desa" placeholder="Masukkan deskripsi singkat program kreatif desa" value={formData.slug} - onChange={(e) => setFormData({ ...formData, slug: e.target.value })} + onChange={(e) => handleChange('slug')(e.target.value)} required /> @@ -133,10 +206,7 @@ function EditProgramKreatifDesa() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - stateProgramKreatif.update.form.deskripsi = htmlContent; - }} + onChange={handleChange('deskripsi')} /> @@ -146,14 +216,22 @@ function EditProgramKreatifDesa() { { - setFormData((prev) => ({ ...prev, icon: value })); - stateProgramKreatif.update.form.icon = value; - }} + onChange={handleChange('icon')} /> + + + {/* Tombol Simpan */} @@ -173,4 +251,4 @@ function EditProgramKreatifDesa() { ); } -export default EditProgramKreatifDesa; +export default EditProgramKreatifDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx index 94dd1015..f681a429 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -86,49 +86,45 @@ function DetailProgramKreatifDesa() { Deskripsi Singkat - {data?.slug || '-'} + {data?.slug || '-'} Deskripsi - + - - - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx index 883529be..b1505836 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx @@ -4,23 +4,26 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; -import programKreatifState from '../../../_state/inovasi/program-kreatif'; import SelectIconProgram from '../../../_com/selectIcon'; +import programKreatifState from '../../../_state/inovasi/program-kreatif'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; function CreateProgramKreatifDesa() { const stateCreate = useProxy(programKreatifState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateCreate.create.form = { @@ -32,20 +35,29 @@ function CreateProgramKreatifDesa() { }; const handleSubmit = async () => { - await stateCreate.create.create(); - resetForm(); - router.push("/admin/inovasi/program-kreatif-desa"); + try { + const success = await stateCreate.create.create(); + + if (success) { + resetForm(); + router.push("/admin/inovasi/program-kreatif-desa"); + } + } catch (error) { + console.error("Error creating program kreatif desa:", error); + toast.error("Gagal menambahkan program kreatif desa"); + } finally { + setIsSubmitting(false); + } }; + return ( {/* Tombol kembali */} - - - + Tambah Program Kreatif Desa @@ -100,6 +112,17 @@ function CreateProgramKreatifDesa() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx index 5ead6694..7b7a647b 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx @@ -18,8 +18,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { IconCash, @@ -117,20 +116,18 @@ function ListProgramKreatifDesa({ search }: { search: string }) { Daftar Program Kreatif Desa - - - +
@@ -164,20 +161,18 @@ function ListProgramKreatifDesa({ search }: { search: string }) { > Daftar Program Kreatif Desa - - - +
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx index d11e7981..898470c5 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx @@ -1,18 +1,18 @@ -/* eslint-disable react-hooks/exhaustive-deps */ "use client"; import { + ActionIcon, Box, Button, Center, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from "@mantine/core"; import { IconArrowBack, @@ -39,13 +39,21 @@ function EditKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - name: keamananState.edit.form.name || "", - deskripsi: keamananState.edit.form.deskripsi || "", - imageId: keamananState.edit.form.imageId || "", + name: "", + deskripsi: "", + imageId: "", }); - // Load data by id + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + imageId: "", + imageUrl: "", + }); + + // Load data sekali pas mount useEffect(() => { const loadData = async () => { const id = params?.id as string; @@ -59,6 +67,12 @@ function EditKeamananLingkungan() { deskripsi: data.deskripsi || "", imageId: data.imageId || "", }); + setOriginalData({ + name: data.name || "", + deskripsi: data.deskripsi || "", + imageId: data.imageId || "", + imageUrl: data.image?.link || "", + }); if (data?.image?.link) { setPreviewImage(data.image.link); @@ -71,16 +85,28 @@ function EditKeamananLingkungan() { }; loadData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [params?.id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { - keamananState.edit.form = { - ...keamananState.edit.form, - name: formData.name, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - }; + setIsSubmitting(true); + let imageId = formData.imageId; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ @@ -93,15 +119,25 @@ function EditKeamananLingkungan() { return toast.error("Gagal upload gambar"); } - keamananState.edit.form.imageId = uploaded.id; + imageId = uploaded.id; } + // update global state hanya sekali pas submit + keamananState.edit.form = { + ...keamananState.edit.form, + name: formData.name, + deskripsi: formData.deskripsi, + imageId, + }; + await keamananState.edit.update(); toast.success("Keamanan Lingkungan berhasil diperbarui!"); router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal"); } catch (error) { console.error("Error updating keamananLingkungan:", error); toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan"); + } finally { + setIsSubmitting(false); } }; @@ -109,16 +145,14 @@ function EditKeamananLingkungan() { {/* Header */} - - - + Edit Keamanan Lingkungan @@ -186,7 +220,39 @@ function EditKeamananLingkungan() { {previewImage ? ( - + + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + ) : (
@@ -195,9 +261,7 @@ function EditKeamananLingkungan() { - setFormData({ ...formData, name: e.target.value }) - } + onChange={(e) => handleChange("name", e.target.value)} label="Judul Keamanan Lingkungan" placeholder="Masukkan judul" required @@ -209,25 +273,34 @@ function EditKeamananLingkungan() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - keamananState.edit.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => handleChange("deskripsi", htmlContent)} /> + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx index 50f49d8c..55b8629b 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -88,41 +88,37 @@ function DetailKeamananLingkungan() { Deskripsi - + {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx index ea5c2604..bdf82527 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { @@ -32,6 +33,7 @@ function CreateKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false) const resetForm = () => { keamananState.create.form = { @@ -44,43 +46,49 @@ function CreateKeamananLingkungan() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true) + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error('Gagal mengupload file'); + } + + keamananState.create.form.imageId = uploaded.id; + + await keamananState.create.create(); + + resetForm(); + router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan data keamanan lingkungan'); + } finally { + setIsSubmitting(false) } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal mengupload file'); - } - - keamananState.create.form.imageId = uploaded.id; - - await keamananState.create.create(); - - resetForm(); - router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal'); }; return ( {/* Header */} - - - + Tambah Data Keamanan Lingkungan @@ -111,7 +119,7 @@ function CreateKeamananLingkungan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -196,6 +222,17 @@ function CreateKeamananLingkungan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx index f4bef5c6..e9791329 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -78,18 +77,16 @@ function ListKeamananLingkungan({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Keamanan Lingkungan - - - + {/* Tabel */} @@ -107,12 +104,16 @@ function ListKeamananLingkungan({ search }: { search: string }) { filteredData.map((item) => ( - - {item.name} - + + + {item.name} + + - + + + - + Edit Kontak Darurat Item @@ -100,7 +132,7 @@ function EditKontakItem() { label="Nama Kontak" placeholder="Masukkan nama kontak" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange('name', e.target.value)} required /> @@ -108,7 +140,7 @@ function EditKontakItem() { label="Nomor Telepon" placeholder="Masukkan nomor telepon" value={formData.nomorTelepon} - onChange={(e) => setFormData({ ...formData, nomorTelepon: e.target.value })} + onChange={(e) => handleChange('nomorTelepon', e.target.value)} required /> @@ -118,14 +150,22 @@ function EditKontakItem() { { - setFormData((prev) => ({ ...prev, icon: value })); - kontakState.update.form.icon = value; - }} + onChange={(value) => handleChange('icon', value)} /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx index 93c858cd..5ca14f61 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx @@ -3,7 +3,7 @@ import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -92,32 +92,28 @@ function DetailKontakDarurat() { {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx index f318bce5..aea32c66 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx @@ -6,20 +6,23 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKontakItem() { const kontakState = useProxy(kontakDarurat.kontakDaruratItem); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { kontakState.create.form = { nama: '', @@ -29,25 +32,31 @@ function CreateKontakItem() { }; const handleSubmit = async () => { - await kontakState.create.create(); - resetForm(); - router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item'); + try { + setIsSubmitting(true); + await kontakState.create.create(); + resetForm(); + router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item'); + } catch (error) { + console.error("Error creating kontak darurat item:", error); + toast.error("Gagal menambahkan kontak darurat item"); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Kontak Darurat Item @@ -91,6 +100,17 @@ function CreateKontakItem() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx index dab73629..52f19b7c 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -79,16 +78,14 @@ function ListKontakItem({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Kontak Darurat Item - - - + {/* Tabel */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx index 6057918b..0e0180e5 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx @@ -1,174 +1,229 @@ /* eslint-disable react-hooks/exhaustive-deps */ -"use client"; +'use client'; -import { IconKey } from "@/app/admin/(dashboard)/_com/iconMap"; -import SelectIconProgramEdit from "@/app/admin/(dashboard)/_com/selectIconEdit"; -import kontakDarurat from "@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan"; -import colors from "@/con/colors"; +import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap'; +import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; +import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan'; +import colors from '@/con/colors'; import { Box, Button, Group, + Loader, MultiSelect, 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"; +} 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 EditKontakDaruratKeamanan() { - const [isLoading, setIsLoading] = useState(true); +type FormData = { + name: string; + icon: IconKey | ''; + kategoriId: string[]; +}; + +export default function EditKontakDaruratKeamanan() { const router = useRouter(); + const params = useParams<{ id: string }>(); const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); - const params = useParams(); - const [formData, setFormData] = useState({ - name: kontakState.update.form.nama || "", - icon: kontakState.update.form.icon || "", - kategoriId: kontakState.update.form.kategoriId || [], + + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: '', + icon: '', + kategoriId: [], + }); + const [originalData, setOriginalData] = useState({ + name: '', + icon: '', + kategoriId: [], }); - // Load data + // 🔁 Load data saat ID berubah useEffect(() => { - const loadData = async () => { + const loadInitialData = async () => { + const id = params?.id; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/keamanan/kontak-darurat'); + return; + } + try { - setIsLoading(true); + // Load dropdown kategori await kontakDarurat.kontakDaruratItem.findMany.load(); - const id = params?.id as string; - if (id) { - const data = await kontakState.update.load(id); - if (data) { - setFormData({ - name: data.nama || "", - icon: data.icon || "", - kategoriId: data.kategoriId || [], - }); - } + + // Load data kontak darurat + const data = await kontakState.update.load(id); + if (!data) { + toast.error('Data tidak ditemukan'); + router.push('/admin/keamanan/kontak-darurat'); + return; } - } catch (error) { - console.error("Error loading data:", error); - toast.error("Gagal memuat data"); - } finally { - setIsLoading(false); + + const initial: FormData = { + name: data.nama || '', + icon: (data.icon as IconKey) || '', + kategoriId: Array.isArray(data.kategoriId) ? data.kategoriId : [], + }; + + setFormData(initial); + setOriginalData(initial); + } catch (err) { + console.error('Gagal memuat data:', err); + toast.error('Gagal memuat data kontak darurat'); + router.push('/admin/keamanan/kontak-darurat'); } }; - loadData(); + loadInitialData(); }, [params?.id]); - // Handle submit + + // Debug: Log the kontakDaruratItem data + useEffect(() => { + if (kontakDarurat.kontakDaruratItem.findMany.data) { + console.log('Kontak Item Data:', kontakDarurat.kontakDaruratItem.findMany.data); + } + }, [kontakDarurat.kontakDaruratItem.findMany.data]); + + // 📝 Update field + const updateField = (field: K, value: FormData[K]) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // ↩️ Reset ke data awal + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Simpan const handleSubmit = async () => { + if (!formData.name.trim()) { + toast.error('Nama kontak darurat wajib diisi'); + return; + } + + if (formData.kategoriId.length === 0) { + toast.error('Pilih minimal satu kontak item'); + return; + } + try { + setIsSubmitting(true); + + // Sinkronisasi ke Valtio state (jika digunakan di `update`) kontakState.update.form = { ...kontakState.update.form, nama: formData.name, icon: formData.icon, kategoriId: formData.kategoriId, }; - await kontakState.update.update(); - toast.success("Kontak Darurat berhasil diperbarui!"); - router.push("/admin/keamanan/kontak-darurat"); + + const success = await kontakState.update.update(); + if (success) { + toast.success('Kontak Darurat berhasil diperbarui!'); + router.push('/admin/keamanan/kontak-darurat'); + } } catch (error) { - console.error("Error updating kontak darurat:", error); - toast.error("Terjadi kesalahan saat memperbarui kontak darurat"); + console.error('Error saat menyimpan:', error); + toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; + // 📋 Daftar opsi kategori untuk MultiSelect + const kategoriOptions = (kontakDarurat.kontakDaruratItem.findMany.data || []).map((item: { id: string; nama: string }) => ({ + value: item.id, + label: item.nama, + })); + return ( - - {/* Header */} + - - - + Edit Kontak Darurat Keamanan - {/* Form */} - {/* Nama kategori */} setFormData({ ...formData, name: e.target.value })} - label="Nama Kontak Darurat" + label={Nama Kontak Darurat} placeholder="Masukkan nama kontak darurat" + value={formData.name} + onChange={(e) => updateField('name', e.target.value)} required /> Kontak Item} + placeholder="Pilih kontak item" + data={kategoriOptions} value={formData.kategoriId} - onChange={(val) => setFormData({ ...formData, kategoriId: val })} - label={Kontak Item} - placeholder={isLoading ? "Memuat data..." : "Pilih kontak item"} - data={ - Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data) - ? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({ - value: v.id, - label: v.nama - })) - : [] - } + onChange={(values) => updateField('kategoriId', values)} clearable searchable required - error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined} - disabled={isLoading} + error={!formData.kategoriId.length ? 'Pilih minimal satu kategori' : undefined} /> - Ikon Program Kreatif Desa + + Ikon Program Kreatif Desa + { - setFormData((prev) => ({ ...prev, icon: value })); - kontakState.update.form.icon = value; - }} + value={formData.icon} + onChange={(icon) => updateField('icon', icon)} /> - {/* Submit */} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} ); -} - -export default EditKontakDaruratKeamanan; +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx index c25192d5..10fb1461 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx @@ -3,7 +3,7 @@ import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -108,32 +108,28 @@ function DetailKontakDaruratKeamanan() { {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx index 4eb42efb..330fa818 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx @@ -6,22 +6,25 @@ import { Box, Button, Group, + Loader, MultiSelect, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKontakDaruratKeamanan() { const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); useShallowEffect(() => { kontakDarurat.kontakDaruratItem.findMany.load(); @@ -36,25 +39,31 @@ function CreateKontakDaruratKeamanan() { }; const handleSubmit = async () => { - await kontakState.create.create(); - resetForm(); - router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan'); + try { + setIsSubmitting(true); + await kontakState.create.create(); + resetForm(); + router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan'); + } catch (error) { + console.error('Error creating kontak darurat:', error); + toast.error('Terjadi kesalahan saat menambah kontak darurat'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Kontak Darurat Keamanan @@ -103,6 +112,17 @@ function CreateKontakDaruratKeamanan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx index a5b6239e..cecb0841 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -79,16 +78,14 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Kontak Darurat Keamanan - - - + {/* Tabel */} diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx index 5d2c2789..7aecff43 100644 --- a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; @@ -8,13 +7,13 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { DateTimePicker } from '@mantine/dates'; import { IconArrowBack } from '@tabler/icons-react'; @@ -29,14 +28,31 @@ function EditLaporanPublik() { const stateLaporan = useProxy(laporanPublikState); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); - const [formData, setFormData] = useState({ - judul: stateLaporan.edit.form.judul || '', - lokasi: stateLaporan.edit.form.lokasi || '', - tanggalWaktu: stateLaporan.edit.form.tanggalWaktu || '', - status: stateLaporan.edit.form.status || '', - penanganan: stateLaporan.edit.form.penanganan || '', - kronologi: stateLaporan.edit.form.kronologi || '', + const [formData, setFormData] = useState<{ + judul: string; + lokasi: string; + tanggalWaktu: string; + status: Status; + penanganan: string; + kronologi: string; + }>({ + judul: '', + lokasi: '', + tanggalWaktu: '', + status: 'Proses', // Default status + penanganan: '', + kronologi: '', + }); + + const [originalData, setOriginalData] = useState({ + judul: '', + lokasi: '', + tanggalWaktu: '', + status: 'Proses', // Default status + penanganan: '', + kronologi: '', }); useEffect(() => { @@ -48,16 +64,24 @@ function EditLaporanPublik() { const data = await stateLaporan.edit.load(id); if (data) { setFormData({ - judul: data.judul || '', - lokasi: data.lokasi || '', - tanggalWaktu: data.tanggalWaktu || '', - status: data.status || '', - penanganan: data.penanganan?.map((p: any) => p.deskripsi)[0] || '', - kronologi: data.kronologi || '', + judul: data.judul ?? '', + lokasi: data.lokasi ?? '', + tanggalWaktu: data.tanggalWaktu ?? '', + status: (data.status as Status) ?? 'Proses', + penanganan: data.penanganan?.[0]?.deskripsi ?? '', + kronologi: data.kronologi ?? '', + }); + setOriginalData({ + judul: data.judul ?? '', + lokasi: data.lokasi ?? '', + tanggalWaktu: data.tanggalWaktu ?? '', + status: (data.status as Status) ?? 'Proses', + penanganan: data.penanganan?.[0]?.deskripsi ?? '', + kronologi: data.kronologi ?? '', }); } } catch (error) { - console.error('Error loading laporan publik:', error); + console.error("Error loading laporan publik:", error); toast.error("Gagal mengambil data laporan publik"); } }; @@ -65,16 +89,30 @@ function EditLaporanPublik() { loadLaporanPublik(); }, [params?.id]); + + + const handleChange = (field: string, value: string | Status) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + lokasi: originalData.lokasi, + tanggalWaktu: originalData.tanggalWaktu, + status: (originalData.status as Status), + penanganan: originalData.penanganan, + kronologi: originalData.kronologi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateLaporan.edit.form = { ...stateLaporan.edit.form, - judul: formData.judul, - lokasi: formData.lokasi, - tanggalWaktu: formData.tanggalWaktu, - status: formData.status, - penanganan: formData.penanganan, - kronologi: formData.kronologi, + ...formData, }; await stateLaporan.edit.update(); @@ -83,6 +121,8 @@ function EditLaporanPublik() { } catch (error) { console.error("Error updating laporan publik:", error); toast.error("Terjadi kesalahan saat memperbarui laporan publik"); + } finally { + setIsSubmitting(false); } }; @@ -90,11 +130,9 @@ function EditLaporanPublik() { {/* Header */} - - - + Edit Laporan Publik @@ -112,7 +150,7 @@ function EditLaporanPublik() { setFormData({ ...formData, judul: e.target.value })} + onChange={(e) => handleChange('judul', e.target.value)} label={Judul Laporan Publik} placeholder="Masukkan judul laporan publik" required @@ -120,7 +158,7 @@ function EditLaporanPublik() { setFormData({ ...formData, lokasi: e.target.value })} + onChange={(e) => handleChange('lokasi', e.target.value)} label={Lokasi Laporan Publik} placeholder="Masukkan lokasi laporan publik" required @@ -129,15 +167,20 @@ function EditLaporanPublik() { - setFormData({ ...formData, tanggalWaktu: val ? val.toString() : '' }) - } + onChange={(value: string | null) => { + if (value) { + const date = new Date(value); + handleChange('tanggalWaktu', date.toISOString()); + } else { + handleChange('tanggalWaktu', ''); + } + }} required />
diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx index 42ab7cc9..cd50b6d9 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx @@ -8,11 +8,12 @@ import { Box, Button, Group, + Loader, Paper, Stack, + Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -24,6 +25,7 @@ function EditPencegahanKriminalitas() { const router = useRouter(); const params = useParams(); const kriminalitasState = useProxy(pencegahanKriminalitasState); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ judul: '', @@ -32,6 +34,14 @@ function EditPencegahanKriminalitas() { linkVideo: '', }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + deskripsiSingkat: '', + linkVideo: '', + }); + + // load data hanya sekali pas id berubah useEffect(() => { const loadKriminalitas = async () => { const id = params?.id as string; @@ -41,10 +51,16 @@ function EditPencegahanKriminalitas() { const data = await kriminalitasState.update.load(id); if (data) { setFormData({ - judul: data.judul || '', - deskripsi: data.deskripsi || '', - deskripsiSingkat: data.deskripsiSingkat || '', - linkVideo: data.linkVideo || '', + judul: data.judul ?? '', + deskripsi: data.deskripsi ?? '', + deskripsiSingkat: data.deskripsiSingkat ?? '', + linkVideo: data.linkVideo ?? '', + }); + setOriginalData({ + judul: data.judul ?? '', + deskripsi: data.deskripsi ?? '', + deskripsiSingkat: data.deskripsiSingkat ?? '', + linkVideo: data.linkVideo ?? '', }); } } catch (error) { @@ -58,6 +74,22 @@ function EditPencegahanKriminalitas() { const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo); + const handleChange = + (field: keyof typeof formData) => + (e: React.ChangeEvent) => { + setFormData((prev) => ({ ...prev, [field]: e.target.value })); + }; + + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + deskripsiSingkat: originalData.deskripsiSingkat, + linkVideo: originalData.linkVideo, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { const converted = convertYoutubeUrlToEmbed(formData.linkVideo); if (!converted) { @@ -66,16 +98,14 @@ function EditPencegahanKriminalitas() { } try { - // Update the form data first + setIsSubmitting(true); + // update global state saat submit kriminalitasState.update.form = { - ...kriminalitasState.update.form, judul: formData.judul, deskripsi: formData.deskripsi, deskripsiSingkat: formData.deskripsiSingkat, linkVideo: formData.linkVideo, }; - - // Set the ID and then call update kriminalitasState.update.id = params?.id as string; await kriminalitasState.update.update(); @@ -84,6 +114,8 @@ function EditPencegahanKriminalitas() { } catch (error) { console.error('Error updating pencegahan kriminalitas:', error); toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } }; @@ -91,16 +123,14 @@ function EditPencegahanKriminalitas() { {/* Back button + Title */} - - - + Edit Pencegahan Kriminalitas @@ -120,19 +150,21 @@ function EditPencegahanKriminalitas() { label="Judul" placeholder="Masukkan judul" value={formData.judul} - onChange={(e) => setFormData({ ...formData, judul: e.target.value })} + onChange={handleChange('judul')} required /> - - setFormData({ ...formData, deskripsiSingkat: e.target.value }) - } - required - /> + + + Deskripsi + + + setFormData((prev) => ({ ...prev, deskripsiSingkat: val })) + } + /> + @@ -141,7 +173,7 @@ function EditPencegahanKriminalitas() { <EditEditor value={formData.deskripsi} onChange={(val) => - setFormData({ ...formData, deskripsi: val }) + setFormData((prev) => ({ ...prev, deskripsi: val })) } /> </Box> @@ -151,9 +183,7 @@ function EditPencegahanKriminalitas() { label="Link Video YouTube" placeholder="https://www.youtube.com/watch?v=abc123" value={formData.linkVideo} - onChange={(e) => - setFormData({ ...formData, linkVideo: e.currentTarget.value }) - } + onChange={handleChange('linkVideo')} required /> {embedLink && ( @@ -175,6 +205,17 @@ function EditPencegahanKriminalitas() { {/* Action button */} <Group justify="right"> + <Button + variant="outline" + color="gray" + radius="md" + size="md" + onClick={handleResetForm} + > + Batal + </Button> + + {/* Tombol Simpan */} <Button onClick={handleSubmit} radius="md" @@ -185,7 +226,7 @@ function EditPencegahanKriminalitas() { boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} > - Simpan + {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'} </Button> </Group> </Stack> diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx index 72371193..6dfdfcb0 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; -import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; -import { useRouter, useParams } from 'next/navigation'; -import { useState } from 'react'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas'; @@ -75,7 +75,7 @@ function DetailPencegahanKriminalitas() { <Box> <Text fz="lg" fw="bold">Deskripsi Singkat</Text> {data?.deskripsiSingkat ? ( - <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat }} /> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat }} /> ) : ( <Text fz="sm" c="dimmed">Tidak ada deskripsi singkat</Text> )} @@ -84,7 +84,7 @@ function DetailPencegahanKriminalitas() { <Box> <Text fz="lg" fw="bold">Deskripsi</Text> {data?.deskripsi ? ( - <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} /> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi }} /> ) : ( <Text fz="sm" c="dimmed">Tidak ada deskripsi</Text> )} @@ -108,32 +108,28 @@ function DetailPencegahanKriminalitas() { {/* Tombol Aksi */} <Flex gap="sm" mt="sm"> - <Tooltip label="Hapus" withArrow position="top"> - <Button - color="red" - onClick={() => { - setSelectedId(data.id); - setModalHapus(true); - }} - variant="light" - radius="md" - size="md" - > - <IconTrash size={20} /> - </Button> - </Tooltip> + <Button + color="red" + onClick={() => { + setSelectedId(data.id); + setModalHapus(true); + }} + variant="light" + radius="md" + size="md" + > + <IconTrash size={20} /> + </Button> - <Tooltip label="Edit" withArrow position="top"> - <Button - color="green" - onClick={() => router.push(`/admin/keamanan/pencegahan-kriminalitas/${data.id}/edit`)} - variant="light" - radius="md" - size="md" - > - <IconEdit size={20} /> - </Button> - </Tooltip> + <Button + color="green" + onClick={() => router.push(`/admin/keamanan/pencegahan-kriminalitas/${data.id}/edit`)} + variant="light" + radius="md" + size="md" + > + <IconEdit size={20} /> + </Button> </Flex> </Stack> </Paper> diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx index 69f06504..50d14fa0 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx @@ -5,27 +5,28 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas'; -import { useState } from 'react'; import { convertYoutubeUrlToEmbed } from '../../../desa/gallery/lib/youtube-utils'; -import { toast } from 'react-toastify'; function CreatePencegahanKriminalitas() { const router = useRouter(); const kriminalitasState = useProxy(pencegahanKriminalitasState); const [link, setLink] = useState(''); const embedLink = convertYoutubeUrlToEmbed(link); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { kriminalitasState.create.form = { @@ -38,22 +39,29 @@ function CreatePencegahanKriminalitas() { }; const handleSubmit = async () => { - if (!embedLink) { - toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); - return; + try { + setIsSubmitting(true); + if (!embedLink) { + toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); + return; + } + + kriminalitasState.create.form.linkVideo = embedLink; + await kriminalitasState.create.create(); + resetForm(); + router.push('/admin/keamanan/pencegahan-kriminalitas'); + } catch (error) { + console.error('Gagal menambahkan pencegahan kriminalitas:', error); + toast.error('Gagal menambahkan pencegahan kriminalitas'); + } finally { + setIsSubmitting(false); } - - kriminalitasState.create.form.linkVideo = embedLink; - await kriminalitasState.create.create(); - resetForm(); - router.push('/admin/keamanan/pencegahan-kriminalitas'); }; return ( <Box px={{ base: 'sm', md: 'lg' }} py="md"> {/* Header Back Button + Title */} <Group mb="md"> - <Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Button variant="subtle" onClick={() => router.back()} @@ -62,7 +70,6 @@ function CreatePencegahanKriminalitas() { > <IconArrowBack color={colors['blue-button']} size={24} /> </Button> - </Tooltip> <Title order={4} ml="sm" c="dark"> Tambah Pencegahan Kriminalitas @@ -90,15 +97,17 @@ function CreatePencegahanKriminalitas() { /> {/* Deskripsi Singkat */} - { - kriminalitasState.create.form.deskripsiSingkat = e.currentTarget.value; - }} - required - /> + + + Deskripsi Singkat + + { + kriminalitasState.create.form.deskripsiSingkat = val; + }} + /> + {/* Deskripsi Panjang */} @@ -141,6 +150,17 @@ function CreatePencegahanKriminalitas() { {/* Button Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx index 45db4790..6bea17ac 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx @@ -16,16 +16,15 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; -import HeaderSearch from '../../_com/header'; -import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; -import pencegahanKriminalitasState from '../../_state/keamanan/pencegahan-kriminalitas'; import { useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../_com/header'; +import pencegahanKriminalitasState from '../../_state/keamanan/pencegahan-kriminalitas'; function PencegahanKriminalitas() { const [search, setSearch] = useState(""); @@ -77,16 +76,14 @@ function ListPencegahanKriminalitas({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Pencegahan Kriminalitas - - - + {/* Tabel */} @@ -105,9 +102,11 @@ function ListPencegahanKriminalitas({ search }: { search: string }) { data.map((item) => ( - - {item.judul} - + + + {item.judul} + + diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx index 4605c3be..736c9e4e 100644 --- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx @@ -2,27 +2,27 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ "use client"; +import polsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat"; import colors from "@/con/colors"; import { Box, Button, Card, Group, + Loader, Modal, Paper, Select, Stack, Text, TextInput, - Title, - Tooltip, + Title } from "@mantine/core"; import { IconArrowBack } from "@tabler/icons-react"; import { useParams, useRouter } from "next/navigation"; -import { useProxy } from "valtio/utils"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; -import polsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat"; +import { useProxy } from "valtio/utils"; function EditPolsekTerdekat() { const polsekState = useProxy(polsekTerdekat); @@ -39,6 +39,7 @@ function EditPolsekTerdekat() { null ); const [namaLayananUpdate, setNamaLayananUpdate] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: "", jarakKeDesa: "", @@ -52,6 +53,20 @@ function EditPolsekTerdekat() { layananPolsekId: "", }); + const [originalData, setOriginalData] = useState({ + nama: "", + jarakKeDesa: "", + alamat: "", + nomorTelepon: "", + jamOperasional: "", + embedMapUrl: "", + namaTempatMaps: "", + alamatMaps: "", + linkPetunjukArah: "", + layananPolsekId: "", + }); + + // load data untuk form edit useEffect(() => { const loadPolsekTerdekat = async () => { const id = params?.id as string; @@ -72,6 +87,19 @@ function EditPolsekTerdekat() { linkPetunjukArah: data.linkPetunjukArah || "", layananPolsekId: data.layananPolsekId || "", }); + + setOriginalData({ + nama: data.nama || "", + jarakKeDesa: data.jarakKeDesa || "", + alamat: data.alamat || "", + nomorTelepon: data.nomorTelepon || "", + jamOperasional: data.jamOperasional || "", + embedMapUrl: data.embedMapUrl || "", + namaTempatMaps: data.namaTempatMaps || "", + alamatMaps: data.alamatMaps || "", + linkPetunjukArah: data.linkPetunjukArah || "", + layananPolsekId: data.layananPolsekId || "", + }); } } catch (error) { console.error("Error loading polsek terdekat:", error); @@ -82,21 +110,6 @@ function EditPolsekTerdekat() { loadPolsekTerdekat(); }, [params?.id]); - const handleSubmit = async () => { - try { - polsekState.edit.form = { - ...polsekState.edit.form, - ...formData, - }; - await polsekState.edit.update(); - toast.success("Polsek terdekat berhasil diperbarui!"); - router.push("/admin/keamanan/polsek-terdekat"); - } catch (error) { - console.error("Error updating polsek terdekat:", error); - toast.error("Gagal memperbarui data polsek terdekat"); - } - }; - const fetchLayanan = async () => { try { const res = await fetch("/api/keamanan/layanan-polsek/find-many"); @@ -198,6 +211,41 @@ function EditPolsekTerdekat() { fetchLayanan(); }, []); + const handleChange = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + jarakKeDesa: originalData.jarakKeDesa, + alamat: originalData.alamat, + nomorTelepon: originalData.nomorTelepon, + jamOperasional: originalData.jamOperasional, + embedMapUrl: originalData.embedMapUrl, + namaTempatMaps: originalData.namaTempatMaps, + alamatMaps: originalData.alamatMaps, + linkPetunjukArah: originalData.linkPetunjukArah, + layananPolsekId: originalData.layananPolsekId, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + polsekState.edit.form = { ...formData }; // update global state hanya di sini + await polsekState.edit.update(); + toast.success("Polsek terdekat berhasil diperbarui!"); + router.push("/admin/keamanan/polsek-terdekat"); + } catch (error) { + console.error("Error updating polsek terdekat:", error); + toast.error("Gagal memperbarui data polsek terdekat"); + } finally { + setIsSubmitting(false); + } + }; + return ( {/* Modal Tambah */} @@ -246,16 +294,14 @@ function EditPolsekTerdekat() { {/* Header */} - - - + Edit Polsek Terdekat @@ -274,87 +320,58 @@ function EditPolsekTerdekat() { {/* Input fields */} - setFormData({ ...formData, nama: val.target.value }) - } + onChange={(e) => handleChange("nama", e.currentTarget.value)} label="Nama Polsek Terdekat" placeholder="Masukkan nama Polsek Terdekat" required /> - setFormData({ ...formData, jarakKeDesa: val.target.value }) - } + onChange={(e) => handleChange("jarakKeDesa", e.currentTarget.value)} label="Jarak Polsek Terdekat" - placeholder="Masukkan jarak Polsek Terdekat" /> - setFormData({ ...formData, alamat: val.target.value }) - } + onChange={(e) => handleChange("alamat", e.currentTarget.value)} label="Alamat Polsek Terdekat" - placeholder="Masukkan alamat Polsek Terdekat" /> - setFormData({ ...formData, nomorTelepon: val.target.value }) - } + onChange={(e) => handleChange("nomorTelepon", e.currentTarget.value)} label="Nomor Telepon" - placeholder="Masukkan nomor telepon Polsek Terdekat" /> - setFormData({ ...formData, jamOperasional: val.target.value }) - } + onChange={(e) => handleChange("jamOperasional", e.currentTarget.value)} label="Jam Operasional" - placeholder="Masukkan jam operasional Polsek Terdekat" /> - setFormData({ ...formData, embedMapUrl: val.target.value }) - } + onChange={(e) => handleChange("embedMapUrl", e.currentTarget.value)} label="Embed Map URL" - placeholder="Masukkan embed map url" /> - setFormData({ ...formData, namaTempatMaps: val.target.value }) - } + onChange={(e) => handleChange("namaTempatMaps", e.currentTarget.value)} label="Nama Tempat Maps" - placeholder="Masukkan nama tempat di maps" /> - setFormData({ ...formData, alamatMaps: val.target.value }) - } + onChange={(e) => handleChange("alamatMaps", e.currentTarget.value)} label="Alamat Maps" - placeholder="Masukkan alamat di maps" /> - setFormData({ ...formData, linkPetunjukArah: val.target.value }) - } + onChange={(e) => handleChange("linkPetunjukArah", e.currentTarget.value)} label="Link Petunjuk Arah" - placeholder="Masukkan link petunjuk arah" /> - {/* Dropdown Layanan */} Layanan Polsek} + label="Layanan Polsek" placeholder="Pilih layanan polsek" data={layananOptions} - value={polsekState.create.form.layananPolsekId} - onChange={(val) => (polsekState.create.form.layananPolsekId = val || "")} + value={polsekState.create.form.layananPolsekId || null} + onChange={(val: string | null) => { + if (val) { + const selected = layananOptions.find( + (item) => item.value === val + ); + if (selected) { + polsekState.create.form.layananPolsekId = selected.value; + } + } else { + polsekState.create.form.layananPolsekId = ''; + } + }} + searchable + clearable + nothingFoundMessage="Tidak ditemukan" + required /> + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx index 22aa1d19..721847d1 100644 --- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -76,16 +75,14 @@ function ListPolsekTerdekat({ search }: { search: string }) { Daftar Polsek Terdekat - - - + @@ -110,11 +107,11 @@ function ListPolsekTerdekat({ search }: { search: string }) { {item.jarakKeDesa} - - + + {item.alamat} - - + + - + Edit Tips Keamanan @@ -137,7 +171,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 +181,11 @@ function EditTipsKeamanan() { > - + @@ -158,14 +198,14 @@ function EditTipsKeamanan() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage ? ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : ( setFormData({ ...formData, judul: e.target.value })} + onChange={(e) => + setFormData((prev) => ({ ...prev, judul: e.target.value })) + } required /> @@ -211,26 +271,37 @@ function EditTipsKeamanan() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - keamananState.update.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> {/* Button Simpan */} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} 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..a241a76d 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import colors from '@/con/colors'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; @@ -68,7 +68,7 @@ function DetailTipsKeamanan() { Nama Tips Keamanan - {data.judul || '-'} + {data.judul || '-'} @@ -76,6 +76,7 @@ function DetailTipsKeamanan() { @@ -98,33 +99,29 @@ 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..898d9905 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -27,6 +28,7 @@ function CreateKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateKeamanan.create.form = { @@ -39,38 +41,44 @@ function CreateKeamananLingkungan() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + stateKeamanan.create.form.imageId = uploaded.id; + + await stateKeamanan.create.create(); + + resetForm(); + router.push('/admin/keamanan/tips-keamanan'); + } catch (error) { + console.error("Error creating tips keamanan:", error); + toast.error("Gagal menambahkan tips keamanan"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - stateKeamanan.create.form.imageId = uploaded.id; - - await stateKeamanan.create.create(); - - resetForm(); - router.push('/admin/keamanan/tips-keamanan'); }; return ( {/* Header Back + Title */} - - - + Tambah Tips Keamanan @@ -101,7 +109,7 @@ function CreateKeamananLingkungan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -122,7 +130,7 @@ function CreateKeamananLingkungan() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -158,6 +184,17 @@ function CreateKeamananLingkungan() { {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx index c01ec070..fe6aeef8 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx @@ -16,14 +16,13 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../_com/header'; import tipsKeamananState from '../../_state/keamanan/tips-keamanan'; @@ -74,16 +73,14 @@ function ListTipsKeamanan({ search }: { search: string }) { Daftar Tips Keamanan - - - +
@@ -99,12 +96,16 @@ function ListTipsKeamanan({ search }: { search: string }) { filteredData.map((item) => ( - - {item.judul} - + + + {item.judul} + + - + + + - + Edit Artikel Kesehatan @@ -170,34 +168,31 @@ function EditArtikelKesehatan() { style={{ border: '1px solid #e0e0e0' }} > - setFormData(prev => ({ ...prev, title: e.target.value }))} + onChange={(value) => setFormData((prev) => ({ ...prev, title: value }))} + placeholder="Masukkan judul artikel" required /> + + {/* Gambar */} - Gambar Berita + Gambar Artikel Kesehatan { - const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error("File tidak valid, gunakan format gambar")} + onDrop={handleFileChange} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ "image/*": [] }} + accept={{ 'image/*': [] }} radius="md" p="xl" > - + @@ -215,64 +210,57 @@ function EditArtikelKesehatan() { - {previewImage && ( - + )} - setFormData(prev => ({ ...prev, content: e.target.value }))} + onChange={(value) => setFormData((prev) => ({ ...prev, content: value }))} + placeholder="Masukkan deskripsi artikel" required /> - - setFormData(prev => ({ - ...prev, - introduction: { ...prev.introduction, content: e.target.value } - })) - } - /> + {/* Pendahuluan */} + + Pendahuluan + + setFormData((prev) => ({ ...prev, introduction: { ...prev.introduction, content: value } })) + } + /> + {/* Gejala */} Gejala - - setFormData(prev => ({ - ...prev, - symptom: { ...prev.symptom, title: e.target.value } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, symptom: { ...prev.symptom, title: value } })) } /> - setFormData(prev => ({ - ...prev, - symptom: { ...prev.symptom, content: e } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, symptom: { ...prev.symptom, content: value } })) } /> @@ -281,23 +269,17 @@ function EditArtikelKesehatan() { {/* Pencegahan */} Pencegahan - - setFormData(prev => ({ - ...prev, - prevention: { ...prev.prevention, title: e.target.value } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, prevention: { ...prev.prevention, title: value } })) } /> - setFormData(prev => ({ - ...prev, - prevention: { ...prev.prevention, content: e } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, prevention: { ...prev.prevention, content: value } })) } /> @@ -305,23 +287,17 @@ function EditArtikelKesehatan() { {/* Pertolongan Pertama */} Pertolongan Pertama - - setFormData(prev => ({ - ...prev, - firstAid: { ...prev.firstAid, title: e.target.value } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, firstAid: { ...prev.firstAid, title: value } })) } /> - setFormData(prev => ({ - ...prev, - firstAid: { ...prev.firstAid, content: e } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, firstAid: { ...prev.firstAid, content: value } })) } /> @@ -329,48 +305,36 @@ function EditArtikelKesehatan() { {/* Mitos vs Fakta */} Mitos vs Fakta - - setFormData(prev => ({ - ...prev, - mythVsFact: { ...prev.mythVsFact, title: e.target.value } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, title: value } })) } /> Mitos - setFormData(prev => ({ - ...prev, - mythVsFact: { ...prev.mythVsFact, mitos: e } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, mitos: value } })) } /> Fakta - setFormData(prev => ({ - ...prev, - mythVsFact: { ...prev.mythVsFact, fakta: e } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, fakta: value } })) } /> - {/* Kapan harus ke dokter */} + {/* Dokter */} Kapan Harus Ke Dokter - setFormData(prev => ({ - ...prev, - doctorSign: { ...prev.doctorSign, content: e } - })) + onChange={(value) => + setFormData((prev) => ({ ...prev, doctorSign: { content: value } })) } /> @@ -384,7 +348,7 @@ function EditArtikelKesehatan() { style={{ background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', - boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)' + boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} > Simpan diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx index 59f980e2..10940912 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx @@ -10,8 +10,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -103,13 +102,13 @@ function DetailArtikelKesehatan() { {/* Deskripsi */} Deskripsi - + {/* Pendahuluan */} Pendahuluan - + {/* Gejala */} @@ -118,7 +117,7 @@ function DetailArtikelKesehatan() { Judul {data.symptom?.title} Deskripsi - + {/* Pencegahan */} @@ -127,7 +126,7 @@ function DetailArtikelKesehatan() { Judul {data.prevention?.title} Deskripsi - + {/* Pertolongan Pertama */} @@ -136,7 +135,7 @@ function DetailArtikelKesehatan() { Judul {data.firstaid?.title} Deskripsi - + {/* Mitos vs Fakta */} @@ -145,49 +144,45 @@ function DetailArtikelKesehatan() { Judul {data.mythvsfact?.title} Mitos - + Fakta - + {/* Kapan ke Dokter */} Kapan Harus ke Dokter - + {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx index 6072684c..3ffdd614 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx @@ -4,16 +4,17 @@ import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -27,6 +28,7 @@ function CreateArtikelKesehatan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateArtikelKesehatan.create.form = { @@ -63,41 +65,46 @@ function CreateArtikelKesehatan() { const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + stateArtikelKesehatan.create.form.imageId = uploaded.id; + await stateArtikelKesehatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan'); + } catch (error) { + console.error('Error submitting form:', error); + toast.error('Gagal menyimpan data, silakan coba lagi'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - stateArtikelKesehatan.create.form.imageId = uploaded.id; - await stateArtikelKesehatan.create.submit(); - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan'); }; return ( {/* Header */} - - - + Tambah Artikel Kesehatan @@ -115,7 +122,7 @@ function CreateArtikelKesehatan() { - Gambar Berita + Gambar Artikel Kesehatan { @@ -127,7 +134,7 @@ function CreateArtikelKesehatan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -148,7 +155,7 @@ function CreateArtikelKesehatan() { {previewImage && ( - + + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} - + - { - stateArtikelKesehatan.create.form.introduction.content = e.target.value; - }} - /> - + + Pendahuluan + { + stateArtikelKesehatan.create.form.introduction.content = e; + }} + /> + {/* Gejala */} Gejala @@ -304,8 +328,20 @@ function CreateArtikelKesehatan() { {/* Submit Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx index 2d1902a0..16091d4a 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -72,16 +71,14 @@ function ListArtikelKesehatan({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Artikel Kesehatan - - - + {/* Tabel */} @@ -89,26 +86,26 @@ function ListArtikelKesehatan({ search }: { search: string }) {
- Judul - Konten - Aksi + Judul + Konten + Aksi {filteredData.length > 0 ? ( filteredData.map((item) => ( - + {item.title} - + {item.content} - + - + Edit Fasilitas Kesehatan @@ -157,161 +191,131 @@ function EditFasilitasKesehatan() { label="Nama Fasilitas Kesehatan" placeholder="Masukkan nama fasilitas kesehatan" value={formData.name} - onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} + onChange={(e) => updateForm('name', e.target.value)} required /> {/* Informasi Umum */} - Informasi Umum + + Informasi Umum + - setFormData(prev => ({ - ...prev, - informasiUmum: { ...prev.informasiUmum, fasilitas: e.target.value }, - })) - } + onChange={(e) => updateNested('informasiUmum', 'fasilitas', e.target.value)} /> - setFormData(prev => ({ - ...prev, - informasiUmum: { ...prev.informasiUmum, alamat: e.target.value }, - })) - } + onChange={(e) => updateNested('informasiUmum', 'alamat', e.target.value)} /> - setFormData(prev => ({ - ...prev, - informasiUmum: { ...prev.informasiUmum, jamOperasional: e.target.value }, - })) - } + onChange={(e) => updateNested('informasiUmum', 'jamOperasional', e.target.value)} /> {/* Layanan Unggulan */} - Layanan Unggulan + + Layanan Unggulan + - setFormData(prev => ({ - ...prev, - layananUnggulan: { content: e }, - })) - } + onChange={(v) => updateNested('layananUnggulan', 'content', v)} /> {/* Dokter dan Tenaga Medis */} - Dokter dan Tenaga Medis + + Dokter dan Tenaga Medis + - setFormData(prev => ({ - ...prev, - dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, name: e.target.value }, - })) - } + onChange={(e) => updateNested('dokterdanTenagaMedis', 'name', e.target.value)} /> - setFormData(prev => ({ - ...prev, - dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, specialist: e.target.value }, - })) + updateNested('dokterdanTenagaMedis', 'specialist', e.target.value) } /> - setFormData(prev => ({ - ...prev, - dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, jadwal: e.target.value }, - })) - } + onChange={(e) => updateNested('dokterdanTenagaMedis', 'jadwal', e.target.value)} /> {/* Fasilitas Pendukung */} - Fasilitas Pendukung + + Fasilitas Pendukung + - setFormData(prev => ({ - ...prev, - fasilitasPendukung: { content: e }, - })) - } + onChange={(v) => updateNested('fasilitasPendukung', 'content', v)} /> {/* Prosedur Pendaftaran */} - Prosedur Pendaftaran + + Prosedur Pendaftaran + - setFormData(prev => ({ - ...prev, - prosedurPendaftaran: { content: e }, - })) - } + onChange={(v) => updateNested('prosedurPendaftaran', 'content', v)} /> {/* Tarif dan Layanan */} - Tarif dan Layanan + + Tarif dan Layanan + - setFormData(prev => ({ - ...prev, - tarifDanLayanan: { ...prev.tarifDanLayanan, tarif: e.target.value }, - })) - } + onChange={(e) => updateNested('tarifDanLayanan', 'tarif', e.target.value)} /> - setFormData(prev => ({ - ...prev, - tarifDanLayanan: { ...prev.tarifDanLayanan, layanan: e.target.value }, - })) - } + onChange={(e) => updateNested('tarifDanLayanan', 'layanan', e.target.value)} /> {/* Tombol Simpan */} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx index 190eb675..5d1e919b 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx @@ -9,8 +9,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -88,24 +87,24 @@ function DetailFasilitasKesehatan() { Fasilitas {data.informasiumum?.fasilitas || '-'} Alamat - {data.informasiumum?.alamat || '-'} + {data.informasiumum?.alamat || '-'} Jam Operasional {data.informasiumum?.jamOperasional || '-'} Layanan Unggulan - + Fasilitas Pendukung - + Prosedur Pendaftaran - + @@ -128,36 +127,32 @@ function DetailFasilitasKesehatan() { {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx index 7d4338c6..4b61acfb 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx @@ -6,15 +6,16 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; @@ -22,6 +23,7 @@ import { useProxy } from 'valtio/utils'; function CreateFasilitasKesehatan() { const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateFasilitasKesehatan.create.form = { @@ -54,26 +56,32 @@ function CreateFasilitasKesehatan() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await stateFasilitasKesehatan.create.submit(); - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'); + try { + setIsSubmitting(true); + await stateFasilitasKesehatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'); + } catch (error) { + console.error(error); + toast.error('Gagal menyimpan data'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Data Fasilitas Kesehatan @@ -196,9 +204,21 @@ function CreateFasilitasKesehatan() { {/* Submit */} - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx index 9c9e3f1e..be757981 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx @@ -5,8 +5,8 @@ import { Button, Center, Group, - Paper, Pagination, + Paper, Skeleton, Stack, Table, @@ -16,11 +16,10 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -29,18 +28,10 @@ import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_wa function FasilitasKesehatan() { - const router = useRouter(); const [search, setSearch] = useState(""); return ( - {/* Tombol Back */} - - - - {/* Header Search */} Daftar Fasilitas Kesehatan - - - + {/* Tabel */} @@ -127,7 +116,9 @@ function ListFasilitasKesehatan({ search }: { search: string }) { - {item.tarifdanlayanan?.layanan || '-'} + + {item.tarifdanlayanan?.layanan || '-'} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx index c5f9bc20..070d52f0 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx @@ -1,36 +1,48 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' + import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +import { convertToISODate } from '../../../persentase_data_kelahiran_kematian/lib/dateUtils'; function EditGrafikHasilKepuasan() { const editState = useProxy(grafikkepuasan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - nama: editState.update.form.nama || '', - tanggal: editState.update.form.tanggal || '', - jenisKelamin: editState.update.form.jenisKelamin || '', - alamat: editState.update.form.alamat || '', - penyakit: editState.update.form.penyakit || '', + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyakit: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyakit: '', + }); + + // Load data once useEffect(() => { const loadData = async () => { const id = params?.id as string; @@ -39,16 +51,26 @@ function EditGrafikHasilKepuasan() { try { const data = await editState.update.load(id); if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + setFormData({ nama: data.nama || '', - tanggal: data.tanggal || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyakit: data.penyakit || '', + }); + + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, jenisKelamin: data.jenisKelamin || '', alamat: data.alamat || '', penyakit: data.penyakit || '', }); } - } catch (error) { - console.error("Error loading grafik hasil kepuasan:", error); + } catch (err) { + console.error("Error loading grafik hasil kepuasan:", err); toast.error("Gagal memuat data grafik hasil kepuasan"); } }; @@ -56,18 +78,34 @@ function EditGrafikHasilKepuasan() { loadData(); }, [params?.id]); + // Generic handler for controlled inputs + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + penyakit: originalData.penyakit, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { - editState.update.form = { - ...editState.update.form, - ...formData, - }; + setIsSubmitting(true); + editState.update.form = { ...editState.update.form, ...formData }; await editState.update.submit(); toast.success('Grafik hasil kepuasan berhasil diperbarui!'); router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan'); - } catch (error) { - console.error('Error updating grafik hasil kepuasan:', error); + } catch (err) { + console.error('Error updating grafik hasil kepuasan:', err); toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan'); + } finally { + setIsSubmitting(false); } }; @@ -75,16 +113,14 @@ function EditGrafikHasilKepuasan() { {/* Header */} - - - + Edit Grafik Hasil Kepuasan @@ -100,46 +136,30 @@ function EditGrafikHasilKepuasan() { style={{ border: '1px solid #e0e0e0' }} > - 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 - /> - setFormData({ ...formData, penyakit: e.target.value })} - label="Penyakit" - placeholder="Masukkan penyakit" - required - /> + {(['nama', 'tanggal', 'jenisKelamin', 'alamat', 'penyakit'] as const).map((field) => ( + handleChange(field, e.target.value)} + type={field === 'tanggal' ? 'date' : 'text'} + label={field === 'jenisKelamin' ? 'Jenis Kelamin' : field.charAt(0).toUpperCase() + field.slice(1)} + placeholder={`Masukkan ${field}`} + required + /> + ))} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx index c80da5f8..738e64fb 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; @@ -101,36 +101,32 @@ function DetailGrafikHasilKepuasan() { {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx index c84f14db..d1ea8b7f 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx @@ -7,22 +7,23 @@ import { Box, Button, Group, + Loader, Paper, Stack, - Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateGrafikHasilKepuasanMasyarakat() { const stateGrafikKepuasan = useProxy(grafikkepuasan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateGrafikKepuasan.create.form = { @@ -35,25 +36,31 @@ function CreateGrafikHasilKepuasanMasyarakat() { }; const handleSubmit = async () => { - await stateGrafikKepuasan.create.create(); - resetForm(); - router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); + try { + setIsSubmitting(true); + await stateGrafikKepuasan.create.create(); + resetForm(); + router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); + } catch (error) { + console.error("Error creating grafik kepuasan:", error); + toast.error("Terjadi kesalahan saat membuat grafik kepuasan"); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Grafik Hasil Kepuasan Masyarakat @@ -107,6 +114,17 @@ function CreateGrafikHasilKepuasanMasyarakat() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx index 16ab706f..e5ba5b28 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx @@ -18,8 +18,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -117,20 +116,18 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Grafik Hasil Kepuasan Masyarakat - - - + {/* Tabel */} @@ -223,7 +220,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) { {/* Chart */} - + Grafik Hasil Kepuasan Masyarakat {mounted && diseaseChartData.length > 0 ? (
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx index a6aacc37..9252f2e4 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx @@ -4,17 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import colors from '@/con/colors'; -import { - Box, - Button, - Group, - Paper, - Stack, - Text, - TextInput, - Title, - Tooltip, -} from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -29,46 +19,52 @@ interface JadwalKegiatanFormBase { waktu: string; lokasi: string; }; - deskripsiJadwalKegiatan: { - deskripsi: string; - }; - layananJadwalKegiatan: { - content: string; - }; - syaratKetentuanJadwalKegiatan: { - content: string; - }; - dokumenJadwalKegiatan: { - content: string; - }; + deskripsiJadwalKegiatan: { deskripsi: string }; + layananJadwalKegiatan: { content: string }; + syaratKetentuanJadwalKegiatan: { content: string }; + dokumenJadwalKegiatan: { content: string }; } +const emptyForm = (): JadwalKegiatanFormBase => ({ + content: '', + informasiJadwalKegiatan: { name: '', tanggal: '', waktu: '', lokasi: '' }, + deskripsiJadwalKegiatan: { deskripsi: '' }, + layananJadwalKegiatan: { content: '' }, + syaratKetentuanJadwalKegiatan: { content: '' }, + dokumenJadwalKegiatan: { content: '' }, +}); + function EditJadwalKegiatan() { const stateJadwalKegiatan = useProxy(jadwalKegiatanState); const router = useRouter(); const params = useParams(); - const [formData, setFormData] = useState({ - content: stateJadwalKegiatan.edit.form.content || '', - informasiJadwalKegiatan: { - name: stateJadwalKegiatan.edit.form.informasiJadwalKegiatan?.name || '', - tanggal: stateJadwalKegiatan.edit.form.informasiJadwalKegiatan?.tanggal || '', - waktu: stateJadwalKegiatan.edit.form.informasiJadwalKegiatan?.waktu || '', - lokasi: stateJadwalKegiatan.edit.form.informasiJadwalKegiatan?.lokasi || '', - }, - deskripsiJadwalKegiatan: { - deskripsi: stateJadwalKegiatan.edit.form.deskripsiJadwalKegiatan?.deskripsi || '', - }, - layananJadwalKegiatan: { - content: stateJadwalKegiatan.edit.form.layananJadwalKegiatan?.content || '', - }, - syaratKetentuanJadwalKegiatan: { - content: stateJadwalKegiatan.edit.form.syaratKetentuanJadwalKegiatan?.content || '', - }, - dokumenJadwalKegiatan: { - content: stateJadwalKegiatan.edit.form.dokumenJadwalKegiatan?.content || '', - }, - }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState(emptyForm()); + const [originalData, setOriginalData] = useState(emptyForm()); + + + // Helper untuk update nested state + const updateNested = < + K extends keyof JadwalKegiatanFormBase, + N extends keyof JadwalKegiatanFormBase[K] + >( + key: K, + subKey: N, + value: string + ) => { + setFormData(prev => ({ + ...prev, + [key]: { + ...(prev[key] as Record), + [subKey]: value + } + })); + }; + + const updateSimple = (key: keyof JadwalKegiatanFormBase, value: string) => { + setFormData(prev => ({ ...prev, [key]: value })); + }; useEffect(() => { const loadJadwalKegiatan = async () => { @@ -80,25 +76,30 @@ function EditJadwalKegiatan() { const { form } = stateJadwalKegiatan.edit; if (form) { setFormData({ - content: form.content, + content: form.content || '', informasiJadwalKegiatan: { name: form.informasiJadwalKegiatan?.name || '', tanggal: form.informasiJadwalKegiatan?.tanggal || '', waktu: form.informasiJadwalKegiatan?.waktu || '', lokasi: form.informasiJadwalKegiatan?.lokasi || '', }, - deskripsiJadwalKegiatan: { - deskripsi: form.deskripsiJadwalKegiatan?.deskripsi || '', - }, - layananJadwalKegiatan: { - content: form.layananJadwalKegiatan?.content || '', - }, - syaratKetentuanJadwalKegiatan: { - content: form.syaratKetentuanJadwalKegiatan?.content || '', - }, - dokumenJadwalKegiatan: { - content: form.dokumenJadwalKegiatan?.content || '', + deskripsiJadwalKegiatan: { deskripsi: form.deskripsiJadwalKegiatan?.deskripsi || '' }, + layananJadwalKegiatan: { content: form.layananJadwalKegiatan?.content || '' }, + syaratKetentuanJadwalKegiatan: { content: form.syaratKetentuanJadwalKegiatan?.content || '' }, + dokumenJadwalKegiatan: { content: form.dokumenJadwalKegiatan?.content || '' }, + }); + setOriginalData({ + content: form.content || '', + informasiJadwalKegiatan: { + name: form.informasiJadwalKegiatan?.name || '', + tanggal: form.informasiJadwalKegiatan?.tanggal || '', + waktu: form.informasiJadwalKegiatan?.waktu || '', + lokasi: form.informasiJadwalKegiatan?.lokasi || '', }, + deskripsiJadwalKegiatan: { deskripsi: form.deskripsiJadwalKegiatan?.deskripsi || '' }, + layananJadwalKegiatan: { content: form.layananJadwalKegiatan?.content || '' }, + syaratKetentuanJadwalKegiatan: { content: form.syaratKetentuanJadwalKegiatan?.content || '' }, + dokumenJadwalKegiatan: { content: form.dokumenJadwalKegiatan?.content || '' }, }); } } catch (error) { @@ -109,18 +110,27 @@ function EditJadwalKegiatan() { loadJadwalKegiatan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + content: originalData.content || '', + informasiJadwalKegiatan: { + name: originalData.informasiJadwalKegiatan?.name || '', + tanggal: originalData.informasiJadwalKegiatan?.tanggal || '', + waktu: originalData.informasiJadwalKegiatan?.waktu || '', + lokasi: originalData.informasiJadwalKegiatan?.lokasi || '', + }, + deskripsiJadwalKegiatan: { deskripsi: originalData.deskripsiJadwalKegiatan?.deskripsi || '' }, + layananJadwalKegiatan: { content: originalData.layananJadwalKegiatan?.content || '' }, + syaratKetentuanJadwalKegiatan: { content: originalData.syaratKetentuanJadwalKegiatan?.content || '' }, + dokumenJadwalKegiatan: { content: originalData.dokumenJadwalKegiatan?.content || '' }, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { - stateJadwalKegiatan.edit.form = { - ...stateJadwalKegiatan.edit.form, - content: formData.content, - informasiJadwalKegiatan: { ...formData.informasiJadwalKegiatan }, - deskripsiJadwalKegiatan: { ...formData.deskripsiJadwalKegiatan }, - layananJadwalKegiatan: { ...formData.layananJadwalKegiatan }, - syaratKetentuanJadwalKegiatan: { ...formData.syaratKetentuanJadwalKegiatan }, - dokumenJadwalKegiatan: { ...formData.dokumenJadwalKegiatan } - }; - + setIsSubmitting(true); + stateJadwalKegiatan.edit.form = { ...stateJadwalKegiatan.edit.form, ...formData }; const success = await stateJadwalKegiatan.edit.submit(); if (success) { toast.success("Jadwal kegiatan berhasil diperbarui!"); @@ -129,6 +139,8 @@ function EditJadwalKegiatan() { } catch (error) { console.error("Error updating jadwal kegiatan:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data jadwal kegiatan"); + } finally { + setIsSubmitting(false); } }; @@ -136,11 +148,9 @@ function EditJadwalKegiatan() { {/* Header */} - - - + Edit Jadwal Kegiatan @@ -161,7 +171,7 @@ function EditJadwalKegiatan() { label="Nama Jadwal Kegiatan" placeholder="Masukkan nama jadwal kegiatan" value={formData.content} - onChange={(e) => setFormData((prev) => ({ ...prev, content: e.target.value }))} + onChange={(e) => updateSimple('content', e.target.value)} /> {/* Deskripsi */} @@ -169,36 +179,22 @@ function EditJadwalKegiatan() { Deskripsi Jadwal Kegiatan setFormData((prev) => ({ - ...prev, - deskripsiJadwalKegiatan: { deskripsi: val } - }))} + onChange={(val) => updateNested('deskripsiJadwalKegiatan', 'deskripsi', val)} /> {/* Informasi Jadwal */} Informasi Jadwal Kegiatan - setFormData((prev) => ({ - ...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, name: e.target.value } - }))} - /> - setFormData((prev) => ({ - ...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, tanggal: e.target.value } - }))} - /> - setFormData((prev) => ({ - ...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, waktu: e.target.value } - }))} - /> - setFormData((prev) => ({ - ...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, lokasi: e.target.value } - }))} - /> + {(['name', 'tanggal', 'waktu', 'lokasi'] as const).map((field) => ( + updateNested('informasiJadwalKegiatan', field, e.target.value)} + /> + ))} {/* Layanan */} @@ -206,10 +202,7 @@ function EditJadwalKegiatan() { Layanan Jadwal Kegiatan setFormData((prev) => ({ - ...prev, - layananJadwalKegiatan: { content: val } - }))} + onChange={(val) => updateNested('layananJadwalKegiatan', 'content', val)} /> @@ -218,10 +211,7 @@ function EditJadwalKegiatan() { Syarat dan Ketentuan setFormData((prev) => ({ - ...prev, - syaratKetentuanJadwalKegiatan: { content: val } - }))} + onChange={(val) => updateNested('syaratKetentuanJadwalKegiatan', 'content', val)} /> @@ -230,15 +220,23 @@ function EditJadwalKegiatan() { Dokumen Yang Perlu Dibawa setFormData((prev) => ({ - ...prev, - dokumenJadwalKegiatan: { content: val } - }))} + onChange={(val) => updateNested('dokumenJadwalKegiatan', 'content', val)} /> {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx index 45a1aade..88e80740 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -83,63 +83,59 @@ function DetailJadwalKegiatan() { Waktu {data.informasijadwalkegiatan.waktu || '-'} Lokasi - {data.informasijadwalkegiatan.lokasi || '-'} + {data.informasijadwalkegiatan.lokasi || '-'} {/* Deskripsi */} Deskripsi - + {/* Layanan */} Layanan - + {/* Syarat Ketentuan */} Syarat Ketentuan - + {/* Dokumen */} Dokumen - + - + {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx index d847720d..4cf617fb 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx @@ -6,21 +6,23 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJadwalKegiatan() { const stateJadwalKegiatan = useProxy(jadwalKegiatanState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateJadwalKegiatan.create.form = { @@ -48,27 +50,32 @@ function CreateJadwalKegiatan() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await stateJadwalKegiatan.create.submit(); - - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan'); + try { + setIsSubmitting(true); + await stateJadwalKegiatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan'); + } catch (error) { + console.error(error); + toast.error('Gagal menyimpan data jadwal kegiatan'); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header */} - - - + Tambah Jadwal Kegiatan @@ -175,8 +182,20 @@ function CreateJadwalKegiatan() { {/* Save Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx index 2114f56f..c8d517a2 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx @@ -5,8 +5,8 @@ import { Button, Center, Group, - Paper, Pagination, + Paper, Skeleton, Stack, Table, @@ -16,30 +16,21 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import jadwalKegiatanState from '../../../_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; -import { useState } from 'react'; function JadwalKegiatan() { - const router = useRouter(); const [search, setSearch] = useState(""); return ( - {/* Tombol Back */} - - - - {/* Header Search */} Daftar Jadwal Kegiatan - - - + {/* Tabel */} @@ -119,14 +108,14 @@ function ListJadwalKegiatan({ search }: { search: string }) { - {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString( - 'id-ID', - { - day: '2-digit', - month: 'long', - year: 'numeric', - } - )} + {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString( + 'id-ID', + { + day: '2-digit', + month: 'long', + year: 'numeric', + } + )} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx index e05848c5..ae458717 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx @@ -1,163 +1,191 @@ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Group, - Paper, - Stack, - TextInput, - Title, - Tooltip + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - +import { convertToISODate } from '../../../lib/dateUtils'; function EditKelahiran() { - const editState = useProxy(persentaseKelahiranKematian.kelahiran); - const router = useRouter(); - const params = useParams(); + const editState = useProxy(persentaseKelahiranKematian.kelahiran); + const router = useRouter(); + const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [formData, setFormData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + }); + + const [originalData, setOriginalData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + }); + + // Load data saat mount atau params.id berubah + useEffect(() => { + const loadKelahiran = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await editState.edit.load(id); + if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + + setFormData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '' + }); + + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '' + }); + } + } catch (error) { + console.error('Error loading data kelahiran:', error); + toast.error('Gagal memuat data kelahiran'); + } + }; + + loadKelahiran(); + }, [params?.id]); + + const handleChange = (key: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [key]: value })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + }); + toast.info("Form dikembalikan ke data awal"); + }; - const [formData, setFormData] = useState({ - nama: editState.edit.form.nama || '', - tanggal: editState.edit.form.tanggal || '', - jenisKelamin: editState.edit.form.jenisKelamin || '', - alamat: editState.edit.form.alamat || '', - }); + const handleSubmit = async () => { + try { + setIsSubmitting(true); + // Update global state hanya saat submit + editState.edit.form = { ...editState.edit.form, ...formData }; + 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'); + } finally { + setIsSubmitting(false); + } + }; + return ( + + {/* Header */} + + + + Edit Data Kelahiran + + - useEffect(() => { - const loadKelahiran = async () => { - const id = params?.id as string; - if (!id) return; + {/* 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 + /> + + - try { - const data = await editState.edit.load(id); - if (data) { - setFormData({ - nama: data.nama || '', - tanggal: data.tanggal || '', - jenisKelamin: data.jenisKelamin || '', - alamat: data.alamat || '', - }); - } - } catch (error) { - console.error('Error loading data kelahiran:', error); - toast.error('Gagal memuat data kelahiran'); - } - }; - - - loadKelahiran(); - }, [params?.id]); - - - 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 - /> - - - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } - -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..549a7fee 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 @@ -1,5 +1,5 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -13,151 +13,147 @@ import colors from '@/con/colors'; function DetailKelahiran() { - const state = useProxy(persentaseKelahiranKematian.kelahiran); - const [modalHapus, setModalHapus] = useState(false); - const [selectedId, setSelectedId] = useState(null); - const params = useParams(); - const router = useRouter(); + const state = useProxy(persentaseKelahiranKematian.kelahiran); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const params = useParams(); + const router = useRouter(); - useShallowEffect(() => { - state.findUnique.load(params?.id as string); - }, []); + useShallowEffect(() => { + state.findUnique.load(params?.id as string); + }, []); - const handleHapus = () => { - if (selectedId) { - state.delete.byId(selectedId); - setModalHapus(false); - setSelectedId(null); - router.push( - "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran" - ); - } - }; + const handleHapus = () => { + if (selectedId) { + state.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push( + "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran" + ); + } + }; - if (!state.findUnique.data) { - return ( - - - - ); - } + if (!state.findUnique.data) { + return ( + + + + ); + } - const data = state.findUnique.data; + const data = state.findUnique.data; - return ( - - {/* Tombol Back */} - + return ( + + {/* Tombol Back */} + - {/* Wrapper Detail */} - - - - Detail Data Kelahiran - + {/* Wrapper Detail */} + + + + Detail Data Kelahiran + - - - - Nama - {data.nama || '-'} - + + + + Nama + {data.nama || '-'} + - - Tanggal - - {new Date(data.tanggal).toLocaleDateString("id-ID", { - day: "2-digit", - month: "long", - year: "numeric", - })} - - + + Tanggal + + {new Date(data.tanggal).toLocaleDateString("id-ID", { + day: "2-digit", + month: "long", + year: "numeric", + })} + + - - Jenis Kelamin - {data.jenisKelamin || '-'} - + + Jenis Kelamin + {data.jenisKelamin || '-'} + - - Alamat - {data.alamat || '-'} - + + Alamat + {data.alamat || '-'} + - {/* Aksi */} - - - - + {/* Aksi */} + + - - - - - - - - + + + + + + - {/* Modal Konfirmasi Hapus */} - setModalHapus(false)} - onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus data ini?" - /> - - ); + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah anda yakin ingin menghapus data ini?" + /> + + ); } 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..90ea1275 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 @@ -2,124 +2,143 @@ 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, + Loader, + Paper, + Stack, + Text, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKelahiran() { - const createState = useProxy(persentaseKelahiranKematian.kelahiran); - const router = useRouter(); + const createState = useProxy(persentaseKelahiranKematian.kelahiran); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const resetForm = () => { + createState.create.form = { + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + }; + }; - const resetForm = () => { - createState.create.form = { - nama: '', - tanggal: '', - jenisKelamin: '', - alamat: '', - }; - }; + const handleSubmit = async () => { + try { + setIsSubmitting(true); + await createState.create.create(); + resetForm(); + router.push( + '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran' + ); + } catch (error) { + console.error('Error creating kelahiran:', error); + toast.error('Gagal menambahkan data kelahiran'); + } finally { + setIsSubmitting(false); + } + }; - const handleSubmit = async () => { - await createState.create.create(); - resetForm(); - router.push( - '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran' - ); - }; + return ( + + {/* Header */} + + + + Tambah Data Kelahiran + + - return ( - - {/* Header */} - - - - - - Tambah Data Kelahiran - - + {/* Form */} + + + Nama} + placeholder="Masukkan nama" + value={createState.create.form.nama} + onChange={(e) => (createState.create.form.nama = e.target.value)} + required + /> + Tanggal} + placeholder="Masukkan tanggal" + value={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} + onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)} + required + /> + Alamat} + placeholder="Masukkan alamat" + value={createState.create.form.alamat} + onChange={(e) => (createState.create.form.alamat = e.target.value)} + required + /> - {/* Form */} - - - Nama} - placeholder="Masukkan nama" - value={createState.create.form.nama} - onChange={(e) => (createState.create.form.nama = e.target.value)} - required - /> - Tanggal} - placeholder="Masukkan tanggal" - value={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} - onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)} - required - /> - Alamat} - placeholder="Masukkan alamat" - value={createState.create.form.alamat} - onChange={(e) => (createState.create.form.alamat = e.target.value)} - required - /> + + - - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx index 255ed68d..13aeebd8 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx @@ -3,23 +3,22 @@ import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Center, - Group, - Paper, - Pagination, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip, + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -29,178 +28,176 @@ import { useProxy } from 'valtio/utils'; function Kelahiran() { - const router = useRouter(); - const [search, setSearch] = useState(""); + const router = useRouter(); + const [search, setSearch] = useState(""); - return ( - - {/* Tombol Back */} - - - + return ( + + {/* Tombol Back */} + + + - {/* Header Search */} - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> + {/* Header Search */} + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> - - - ); + + + ); } function ListKelahiran({ search }: { search: string }) { - const statePersentase = useProxy(persentasekelahiran.kelahiran); - const router = useRouter(); + const statePersentase = useProxy(persentasekelahiran.kelahiran); + const router = useRouter(); - const { data, page, totalPages, loading, load } = statePersentase.findMany; + const { data, page, totalPages, loading, load } = statePersentase.findMany; - useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + useShallowEffect(() => { + load(page, 10, search); + }, [page, search]); - const filteredData = data || []; + const filteredData = data || []; - if (loading || !data) { - return ( - - - - ); - } + if (loading || !data) { + return ( + + + + ); + } - return ( - - - {/* Judul + Tombol Tambah */} - - Daftar Data Kelahiran - - - - + return ( + + + {/* Judul + Tombol Tambah */} + + Daftar Data Kelahiran + + - {/* Tabel */} - -
- - - Nama - Tanggal - Jenis Kelamin - Alamat - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - - {item.nama} - - - - - - {new Date(item.tanggal).toLocaleDateString('id-ID', { - day: '2-digit', - month: 'long', - year: 'numeric', - })} - - - - - {item.jenisKelamin} - - - - - - {item.alamat} - - - - - - - - )) - ) : ( - - -
- - Tidak ada data kelahiran yang cocok - -
-
-
- )} -
-
-
-
+ {/* Tabel */} + + + + + Nama + Tanggal + Jenis Kelamin + Alamat + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + {item.nama} + + + + + + {new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + + + + + {item.jenisKelamin} + + + + + + {item.alamat} + + + + + + + + )) + ) : ( + + +
+ + Tidak ada data kelahiran yang cocok + +
+
+
+ )} +
+
+
+ - {/* Pagination */} -
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
- ); + {/* Pagination */} +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+
+ ); } 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..2606a523 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,179 +1,212 @@ /* 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, + Loader, + Paper, + Stack, + Text, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - +import { convertToISODate } from '../../../lib/dateUtils'; 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 [isSubmitting, setIsSubmitting] = useState(false); + 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 || '', - }); + const [originalData, setOriginalData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyebab: '', + }); + // Load data saat mount + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) return; - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) return; + try { + const data = await editState.edit.load(id); + if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + setFormData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyebab: data.penyebab || '' + }); - 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'); - } - }; + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyebab: data.penyebab || '' + }); + } + } catch (error) { + console.error('Error loading data kematian:', error); + toast.error('Gagal memuat data kematian'); + } + }; + loadData(); + }, [params?.id]); - loadData(); - }, [params?.id]); + const handleChange = (key: keyof typeof formData, value: string) => { + setFormData(prev => ({ ...prev, [key]: value })); + }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + penyebab: originalData.penyebab, + }); + toast.info("Form dikembalikan ke data awal"); + }; - 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'); - } - }; + const handleSubmit = async () => { + try { + setIsSubmitting(true); + // 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'); + } finally { + setIsSubmitting(false); + } + }; + return ( + + {/* Header */} + + + + Edit Data Kematian + + - return ( - - {/* Header dengan tombol back */} - - - - - - Edit Data Kematian - - + {/* Form Card */} + + + handleChange('nama', e.target.value)} + required + /> + handleChange('tanggal', e.target.value)} + required + /> - {/* Card Form */} - - - setFormData({ ...formData, nama: e.target.value })} - required - /> + handleChange('jenisKelamin', e.target.value)} + required + /> + handleChange('alamat', e.target.value)} + required + /> - setFormData({ ...formData, tanggal: e.target.value })} - required - /> + + + Penyebab + + handleChange('penyebab', htmlContent)} + /> + + + - 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; - }} - /> - - - - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } - -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..1cab8f69 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 @@ -1,5 +1,5 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -102,46 +102,42 @@ function DetailKematian() { Alamat - {data?.alamat || '-'} + {data?.alamat || '-'} Penyebab - + - - - + - - - + @@ -155,7 +151,7 @@ function DetailKematian() { onConfirm={handleHapus} text="Apakah Anda yakin ingin menghapus data ini?" /> -
+
); } 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..7248597a 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 @@ -3,143 +3,156 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Group, - Paper, - Stack, - TextInput, - Title, - Tooltip + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - - - - - function CreateKematian() { - const createState = useProxy(persentaseKelahiranKematian.kematian); - const router = useRouter(); + const createState = useProxy(persentaseKelahiranKematian.kematian); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const resetForm = () => { + createState.create.form = { + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyebab: '', + }; + }; - const resetForm = () => { - createState.create.form = { - nama: '', - tanggal: '', - jenisKelamin: '', - alamat: '', - penyebab: '', - }; - }; + const handleSubmit = async () => { + try { + setIsSubmitting(true); + if (!createState.create.form.nama) { + return toast.warn('Nama wajib diisi'); + } + if (!createState.create.form.tanggal) { + return toast.warn('Tanggal wajib diisi'); + } - const handleSubmit = async () => { - if (!createState.create.form.nama) { - return toast.warn('Nama wajib diisi'); - } - if (!createState.create.form.tanggal) { - return toast.warn('Tanggal wajib diisi'); - } + await createState.create.create(); + resetForm(); + router.push( + '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian' + ); + } catch (error) { + console.error('Error creating data kematian:', error); + toast.error('Gagal menambahkan data kematian'); + } finally { + setIsSubmitting(false); + } + }; - await createState.create.create(); - resetForm(); - router.push( - '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian' - ); - }; + return ( + + {/* Header */} + + + + Tambah Data Kematian + + - return ( - - {/* Header */} - - - - - - Tambah Data Kematian - - + {/* Form Card */} + + + (createState.create.form.nama = e.target.value)} + required + /> + (createState.create.form.tanggal = e.target.value)} + required + /> + (createState.create.form.jenisKelamin = e.target.value)} + required + /> + (createState.create.form.alamat = e.target.value)} + required + /> + + + Penyebab + + { + createState.create.form.penyebab = htmlContent; + }} + /> + - {/* Form Card */} - - - (createState.create.form.nama = e.target.value)} - required - /> - (createState.create.form.tanggal = e.target.value)} - required - /> - (createState.create.form.jenisKelamin = e.target.value)} - required - /> - (createState.create.form.alamat = e.target.value)} - required - /> - - - Penyebab - - { - createState.create.form.penyebab = htmlContent; - }} - /> - + + - - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx index b9ab8ec7..52c3a269 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx @@ -3,23 +3,22 @@ import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Center, - Group, - Pagination, - Paper, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip, + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -29,176 +28,174 @@ import { useProxy } from 'valtio/utils'; function Kematian() { - const [search, setSearch] = useState(""); - const router = useRouter(); + const [search, setSearch] = useState(""); + const router = useRouter(); - return ( - - {/* Tombol Back */} - - - + return ( + + {/* Tombol Back */} + + + - {/* Header dengan Search */} - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> + {/* Header dengan Search */} + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> - - - ); + + + ); } function ListKematian({ search }: { search: string }) { - const statePersentase = useProxy(persentasekelahiran.kematian); - const router = useRouter(); + const statePersentase = useProxy(persentasekelahiran.kematian); + const router = useRouter(); - const { data, page, totalPages, loading, load } = statePersentase.findMany; + const { data, page, totalPages, loading, load } = statePersentase.findMany; - useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + useShallowEffect(() => { + load(page, 10, search); + }, [page, search]); - const filteredData = data || []; + const filteredData = data || []; - if (loading || !data) { - return ( - - - - ); - } + if (loading || !data) { + return ( + + + + ); + } - return ( - - - - Daftar Data Kematian - - - - + return ( + + + + Daftar Data Kematian + + - - - - - Nama - Tanggal - Jenis Kelamin - Alamat - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - - {item.nama} - - - - - - {new Date(item.tanggal).toLocaleDateString('id-ID', { - day: '2-digit', - month: 'long', - year: 'numeric', - })} - - - - - {item.jenisKelamin} - - - - - - {item.alamat} - - - - - - - - )) - ) : ( - - -
- - Tidak ada data kematian yang cocok - -
-
-
- )} -
-
-
-
+ + + + + Nama + Tanggal + Jenis Kelamin + Alamat + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + {item.nama} + + + + + + {new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + + + + + {item.jenisKelamin} + + + + + + {item.alamat} + + + + + + + + )) + ) : ( + + +
+ + Tidak ada data kematian yang cocok + +
+
+
+ )} +
+
+
+
- {/* Pagination */} -
- { - load(newPage, 10, search); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
- ); + {/* Pagination */} +
+ { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+ + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx new file mode 100644 index 00000000..3ce629bc --- /dev/null +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx @@ -0,0 +1,24 @@ +export const convertToISODate = (dateString: string): string => { + if (!dateString) return ''; + + // Jika format dd/mm/yyyy + const parts = dateString.split('/'); + if (parts.length === 3 && parts[0].length === 2 && parts[1].length === 2 && parts[2].length === 4) { + const [day, month, year] = parts; + return `${year}-${month}-${day}`; + } + + // Jika sudah format YYYY-MM-DD, biarkan + if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { + return dateString; + } + + // Jika format lain, coba parse dengan Date + const date = new Date(dateString); + if (!isNaN(date.getTime())) { + return date.toISOString().split('T')[0]; // YYYY-MM-DD + } + + console.warn(`Format tanggal tidak dikenali: ${dateString}`); + return ''; +}; \ No newline at end of file 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..39473173 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 @@ -5,21 +5,22 @@ import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wab import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { ChangeEvent, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; @@ -27,16 +28,31 @@ function EditInfoWabahPenyakit() { const infoWabahPenyakitState = useProxy(infoWabahPenyakit); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); - 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 || '', + name: '', + deskripsiSingkat: '', + deskripsiLengkap: '', + imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsiSingkat: '', + deskripsiLengkap: '', + imageId: '', + imageUrl: '' + }); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + + // 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; @@ -48,16 +64,21 @@ function EditInfoWabahPenyakit() { setFormData({ name: data.name || '', deskripsiSingkat: data.deskripsiSingkat || '', - deskripsi: data.deskripsiLengkap || '', + deskripsiLengkap: data.deskripsiLengkap || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsiLengkap: data.deskripsiLengkap || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + if (data.image?.link) setPreviewImage(data.image.link); } } catch (error) { - console.error('Error loading info wabah penyakit:', error); + console.error(error); toast.error('Gagal memuat data info wabah penyakit'); } }; @@ -67,31 +88,55 @@ function EditInfoWabahPenyakit() { const handleSubmit = async () => { try { - infoWabahPenyakitState.edit.form = { - ...infoWabahPenyakitState.edit.form, - name: formData.name, - deskripsiSingkat: formData.deskripsiSingkat, - deskripsiLengkap: formData.deskripsi, - imageId: formData.imageId, - }; + setIsSubmitting(true); + 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'); - } - - infoWabahPenyakitState.edit.form.imageId = uploaded.id; + 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.deskripsiLengkap, + imageId: uploadedImageId, + }; + 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'); + } finally { + setIsSubmitting(false); + } + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsiSingkat: originalData.deskripsiSingkat, + deskripsiLengkap: originalData.deskripsiLengkap, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleDrop = (files: File[]) => { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); } }; @@ -99,11 +144,9 @@ function EditInfoWabahPenyakit() { {/* Header */} - - - + Edit Info Wabah Penyakit @@ -121,27 +164,29 @@ 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)} + /> + - Deskripsi + Deskripsi Lengkap setFormData({ ...formData, deskripsi: val })} + value={formData.deskripsiLengkap} + onChange={(val) => updateField('deskripsiLengkap', val)} /> @@ -150,16 +195,10 @@ 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/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -184,7 +223,7 @@ function EditInfoWabahPenyakit() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} 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..675b4e8a 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 @@ -3,21 +3,20 @@ import colors from '@/con/colors'; import { Box, Button, - Paper, - Stack, - Text, - Skeleton, - Tooltip, Group, Image, + Paper, + Skeleton, + Stack, + Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; function DetailInfoWabahPenyakit() { const state = useProxy(infoWabahPenyakit); @@ -84,7 +83,7 @@ function DetailInfoWabahPenyakit() { Deskripsi Singkat - {data.deskripsiSingkat || '-'} + @@ -93,6 +92,7 @@ function DetailInfoWabahPenyakit() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -113,36 +113,32 @@ function DetailInfoWabahPenyakit() { {/* Aksi */} - - - + - - - + 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..d51ddc31 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 @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -20,13 +22,13 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; -import { Dropzone } from '@mantine/dropzone'; function CreateInfoWabahPenyakit() { const router = useRouter(); const infoWabahPenyakitState = useProxy(infoWabahPenyakit) const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { infoWabahPenyakitState.create.form = { @@ -40,41 +42,47 @@ function CreateInfoWabahPenyakit() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn("Pilih file gambar terlebih dahulu"); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error("Gagal upload gambar"); + } + + infoWabahPenyakitState.create.form.imageId = uploaded.id; + await infoWabahPenyakitState.create.create(); + + resetForm(); + router.push("/admin/kesehatan/info-wabah-penyakit") + } catch (error) { + console.error("Error creating info wabah penyakit:", error); + toast.error("Gagal menambahkan info wabah penyakit"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } - - infoWabahPenyakitState.create.form.imageId = uploaded.id; - await infoWabahPenyakitState.create.create(); - - resetForm(); - router.push("/admin/kesehatan/info-wabah-penyakit") }; return ( {/* Header */} - - - + Tambah Info Wabah Penyakit @@ -100,15 +108,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 @@ -132,7 +140,7 @@ function CreateInfoWabahPenyakit() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -157,7 +165,7 @@ function CreateInfoWabahPenyakit() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} 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..c5ca0547 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -76,16 +75,14 @@ function ListInfoWabahPenyakit({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Info Wabah Penyakit - - - + {/* Tabel */} @@ -111,9 +108,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..21af40ca 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,20 +1,21 @@ /* 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'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -23,7 +24,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(); @@ -31,12 +31,23 @@ function EditKontakDarurat() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - name: kontakDaruratState.edit.form.name || '', - deskripsi: kontakDaruratState.edit.form.deskripsi || '', - imageId: kontakDaruratState.edit.form.imageId || '', + name: '', + deskripsi: '', + imageId: '', + whatsapp: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + whatsapp: '', + imageUrl: '', + }); + const [loading, setLoading] = useState(true); + // Load data sekali saat mount useEffect(() => { const loadKontakDarurat = async () => { const id = params?.id as string; @@ -49,15 +60,22 @@ function EditKontakDarurat() { name: data.name || '', deskripsi: data.deskripsi || '', imageId: data.imageId || '', + whatsapp: data.whatsapp || '', }); - - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + whatsapp: data.whatsapp || '', + imageUrl: 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); } }; @@ -66,48 +84,60 @@ function EditKontakDarurat() { const handleSubmit = async () => { try { - kontakDaruratState.edit.form = { - ...kontakDaruratState.edit.form, - name: formData.name, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - }; + setIsSubmitting(true); + 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"); } catch (error) { console.error("Error updating kontak darurat:", error); toast.error("Terjadi kesalahan saat memperbarui kontak darurat"); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + whatsapp: originalData.whatsapp, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + if (loading) return Loading...; + return ( - {/* Header */} - - - + Edit Kontak Darurat - {/* Form */} + {/* Controlled Input */} setFormData({ ...formData, name: e.target.value })} + onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} label="Judul" placeholder="Masukkan judul" required /> + setFormData(prev => ({ ...prev, whatsapp: e.target.value }))} + label="Whatsapp" + placeholder="Masukkan whatsapp" + required + /> + Deskripsi setFormData({ ...formData, deskripsi: val })} + onChange={(val) => setFormData(prev => ({ ...prev, deskripsi: val }))} /> @@ -145,7 +184,7 @@ function EditKontakDarurat() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -166,7 +205,7 @@ function EditKontakDarurat() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx index a1088693..ea22b9e3 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -72,12 +72,18 @@ function DetailKontakDarurat() { {data.name || '-'} + + Whatsapp + {data.whatsapp || '-'} + + Deskripsi @@ -98,35 +104,31 @@ function DetailKontakDarurat() { {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx index d1767f6a..d35f1fb4 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, @@ -25,61 +27,68 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat'; -import { Dropzone } from '@mantine/dropzone'; function CreateKontakDarurat() { const router = useRouter(); const kontakDaruratState = useProxy(kontakDarurat); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { kontakDaruratState.create.form = { name: '', deskripsi: '', imageId: '', + whatsapp: '', }; setPreviewImage(null); setFile(null); }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + kontakDaruratState.create.form.imageId = uploaded.id; + + await kontakDaruratState.create.create(); + + resetForm(); + router.push('/admin/kesehatan/kontak-darurat'); + } catch (error) { + console.error('Error creating kontak darurat:', error); + toast.error('Gagal menambahkan kontak darurat'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - kontakDaruratState.create.form.imageId = uploaded.id; - - await kontakDaruratState.create.create(); - - resetForm(); - router.push('/admin/kesehatan/kontak-darurat'); }; return ( {/* Header */} - - - + Tambah Kontak Darurat @@ -105,6 +114,17 @@ function CreateKontakDarurat() { required /> + { + kontakDaruratState.create.form.whatsapp = val.target.value; + }} + label={Whatsapp} + placeholder="Masukkan whatsapp" + required + /> + Deskripsi toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx index 2c341cf0..6a8a42b8 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -73,16 +72,14 @@ function ListKontakDarurat({ search }: { search: string }) { Daftar Kontak Darurat - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx index 5e9921b8..ecf4d166 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx @@ -1,20 +1,22 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' + import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -27,17 +29,28 @@ function EditPenangananDarurat() { const penangananDaruratState = useProxy(penangananDarurat) const router = useRouter(); const params = useParams() + const [isSubmitting, setIsSubmitting] = useState(false); + + const [formData, setFormData] = useState({ + name: '', + deskripsi: '', + imageId: '', + }); + + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + imageUrl: '', + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); - const [formData, setFormData] = useState({ - name: penangananDaruratState.edit.form.name || '', - deskripsi: penangananDaruratState.edit.form.deskripsi || '', - imageId: penangananDaruratState.edit.form.imageId || '', - }) + const [loading, setLoading] = useState(true); + // Load data satu kali saat component mount useEffect(() => { - const loadPenangananDarurat = async () => { + const loadData = async () => { const id = params?.id as string; if (!id) return; @@ -48,59 +61,95 @@ function EditPenangananDarurat() { name: data.name || '', deskripsi: data.deskripsi || '', imageId: data.imageId || '', - }) + }); - if (data?.image?.link) { + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); + + if (data.image?.link) { setPreviewImage(data.image.link); } } - } catch (error) { - console.error('Error loading penanganan darurat:', error); + } catch (err) { + console.error('Error loading penanganan darurat:', err); toast.error('Gagal memuat data penanganan darurat'); + } finally { + setLoading(false); } } - loadPenangananDarurat(); - }, [params?.id]) + loadData(); + }, [params?.id]); + + const handleChange = (key: keyof typeof formData, value: string) => { + setFormData(prev => ({ ...prev, [key]: value })); + }; + + const handleDrop = (files: File[]) => { + const selected = files[0]; + if (!selected) return; + + setFile(selected); + setPreviewImage(URL.createObjectURL(selected)); + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; const handleSubmit = async () => { try { - penangananDaruratState.edit.form = { - ...penangananDaruratState.edit.form, - name: formData.name, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - } + setIsSubmitting(true); + let imageId = formData.imageId; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ 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"); - penangananDaruratState.edit.form.imageId = uploaded.id; + imageId = uploaded.id; } + // update global state sekali saat submit + penangananDaruratState.edit.form = { + ...penangananDaruratState.edit.form, + name: formData.name, + deskripsi: formData.deskripsi, + imageId, + }; + await penangananDaruratState.edit.update(); toast.success("Penanganan darurat berhasil diperbarui!"); router.push("/admin/kesehatan/penanganan-darurat"); - } catch (error) { - console.error("Error updating penanganan darurat:", error); + } catch (err) { + console.error("Error updating penanganan darurat:", err); toast.error("Gagal memperbarui data penanganan darurat"); + } finally { + setIsSubmitting(false); } - } + }; + + if (loading) return Loading...; return ( {/* Header */} - - - + Edit Penanganan Darurat @@ -118,7 +167,7 @@ function EditPenangananDarurat() { setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange('name', e.target.value)} label="Judul" placeholder="Masukkan judul" required @@ -128,23 +177,17 @@ function EditPenangananDarurat() { Deskripsi setFormData({ ...formData, deskripsi: val })} + onChange={(val) => handleChange('deskripsi', val)} /> 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/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -169,7 +212,7 @@ function EditPenangananDarurat() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx index c7f11f9b..ab622c6e 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx @@ -1,11 +1,11 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip, Image } from '@mantine/core'; -import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; -import { useRouter, useParams } from 'next/navigation'; -import React, { useState } from 'react'; -import { useProxy } from 'valtio/utils'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat'; @@ -78,6 +78,7 @@ function DetailPenangananDarurat() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -95,34 +96,30 @@ function DetailPenangananDarurat() { {/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx index 89dc64ff..c75878fb 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { @@ -31,6 +32,7 @@ function CreatePenangananDarurat() { const router = useRouter(); const penangananDaruratState = useProxy(penangananDarurat); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [file, setFile] = useState(null); const resetForm = () => { @@ -44,42 +46,48 @@ function CreatePenangananDarurat() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + penangananDaruratState.create.form.imageId = uploaded.id; + + await penangananDaruratState.create.create(); + + resetForm(); + router.push('/admin/kesehatan/penanganan-darurat'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan penanganan darurat'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - penangananDaruratState.create.form.imageId = uploaded.id; - - await penangananDaruratState.create.create(); - - resetForm(); - router.push('/admin/kesehatan/penanganan-darurat'); }; return ( {/* Header */} - - - + Tambah Penanganan Darurat @@ -131,7 +139,7 @@ function CreatePenangananDarurat() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -193,6 +219,17 @@ function CreatePenangananDarurat() { {/* Button Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx index 7d397e4d..a845350e 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -71,16 +70,14 @@ function ListPenangananDarurat({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Penanganan Darurat - - - + {/* Tabel */} @@ -111,8 +108,8 @@ function ListPenangananDarurat({ search }: { search: string }) { c="dimmed" truncate lineClamp={1} - dangerouslySetInnerHTML={{ __html: item.deskripsi }} - /> + dangerouslySetInnerHTML={{ __html: item.deskripsi }} + /> diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx index 606f2538..8882f40a 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx @@ -1,20 +1,22 @@ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { - Box, - Button, - Group, - Image, - Paper, - Stack, - Text, - TextInput, - Title, - Tooltip, + ActionIcon, + Box, + Button, + Group, + Image, + Loader, + Paper, + Stack, + Text, + TextInput, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -23,234 +25,272 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - function EditPosyandu() { - const statePosyandu = useProxy(posyandustate); - const router = useRouter(); - const params = useParams(); + const statePosyandu = useProxy(posyandustate); + const router = useRouter(); + const params = useParams(); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: '', + nomor: '', + deskripsi: '', + imageId: '', + jadwalPelayanan: '', + }); + const [originalData, setOriginalData] = useState({ + name: "", + nomor: "", + deskripsi: "", + imageId: "", + jadwalPelayanan: "", + imageUrl: "" + }); - const [previewImage, setPreviewImage] = useState(null); - const [file, setFile] = useState(null); - const [formData, setFormData] = useState({ - name: statePosyandu.edit.form.name || '', - nomor: statePosyandu.edit.form.nomor || '', - deskripsi: statePosyandu.edit.form.deskripsi || '', - imageId: statePosyandu.edit.form.imageId || '', - jadwalPelayanan: statePosyandu.edit.form.jadwalPelayanan || '', - }); + // Load data posyandu + useEffect(() => { + const loadPosyandu = async () => { + const id = params?.id as string; + if (!id) return; + try { + const data = await statePosyandu.edit.load(id); + if (data) { + setFormData({ + name: data.name || '', + nomor: data.nomor || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + jadwalPelayanan: data.jadwalPelayanan || '', + }); + setOriginalData({ + name: data.name || '', + nomor: data.nomor || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + jadwalPelayanan: data.jadwalPelayanan || '', + imageUrl: data.image?.link || '', + }); + if (data?.image?.link) setPreviewImage(data.image.link); + } + } catch (error) { + console.error('Error loading posyandu:', error); + toast.error('Gagal memuat data posyandu'); + } + }; + loadPosyandu(); + }, [params?.id]); - useEffect(() => { - const loadPosyandu = async () => { - const id = params?.id as string; - if (!id) return; + const handleSubmit = async () => { + try { + setIsSubmitting(true); + const updatedForm = { ...statePosyandu.edit.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; - try { - const data = await statePosyandu.edit.load(id); - if (data) { - setFormData({ - name: data.name || '', - nomor: data.nomor || '', - deskripsi: data.deskripsi || '', - imageId: data.imageId || '', - jadwalPelayanan: data.jadwalPelayanan || '', - }); + if (!uploaded?.id) return toast.error('Gagal upload gambar'); + updatedForm.imageId = uploaded.id; + } + statePosyandu.edit.form = updatedForm; + await statePosyandu.edit.update(); - if (data?.image?.link) { - setPreviewImage(data.image.link); - } - } - } catch (error) { - console.error("Error loading posyandu:", error); - toast.error("Gagal memuat data posyandu"); - } - }; - loadPosyandu(); - }, [params?.id]); + 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'); + } finally { + setIsSubmitting(false); + } + }; + const resetForm = () => { + setFormData({ + name: originalData.name, + nomor: originalData.nomor, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + jadwalPelayanan: originalData.jadwalPelayanan, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; - const handleSubmit = async () => { - try { - statePosyandu.edit.form = { - ...statePosyandu.edit.form, - ...formData, - }; + 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/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + + + + - if (file) { - const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); - const uploaded = res.data?.data; + {previewImage && ( + + Preview Gambar + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } + {/* Input Form */} + setFormData({ ...formData, name: e.target.value })} + required + /> + setFormData({ ...formData, nomor: e.target.value })} + required + /> - statePosyandu.edit.form.imageId = uploaded.id; - } + + + Deskripsi Posyandu + + setFormData({ ...formData, deskripsi: htmlContent })} + /> + + + + Jadwal Pelayanan + + + setFormData({ ...formData, jadwalPelayanan: htmlContent }) + } + /> + - 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"); - } - }; + {/* Tombol Submit */} + + - - 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 && ( - - Preview Gambar - - )} - - - - {/* 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 Simpan */} + + + + + + ); } - -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..f75cda27 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx @@ -1,175 +1,173 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core'; -import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import posyanduState from '../../../_state/kesehatan/posyandu/posyandu'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import posyanduState from '../../../_state/kesehatan/posyandu/posyandu'; function DetailPosyandu() { - const statePosyandu = useProxy(posyanduState); - const params = useParams(); - const router = useRouter(); - const [modalHapus, setModalHapus] = useState(false); - const [selectedId, setSelectedId] = useState(null); + const statePosyandu = useProxy(posyanduState); + const params = useParams(); + const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); - useShallowEffect(() => { - statePosyandu.findUnique.load(params?.id as string); - }, []); + useShallowEffect(() => { + statePosyandu.findUnique.load(params?.id as string); + }, []); - const handleHapus = () => { - if (selectedId) { - statePosyandu.delete.byId(selectedId); - setModalHapus(false); - setSelectedId(null); - router.push("/admin/kesehatan/posyandu"); - } - }; + const handleHapus = () => { + if (selectedId) { + statePosyandu.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push("/admin/kesehatan/posyandu"); + } + }; - if (!statePosyandu.findUnique.data) { - return ( - - - - ); - } + if (!statePosyandu.findUnique.data) { + return ( + + + + ); + } - const data = statePosyandu.findUnique.data; + const data = statePosyandu.findUnique.data; - return ( - - {/* Tombol kembali */} - + return ( + + {/* Tombol kembali */} + - {/* Card utama */} - - - - Detail Posyandu - + {/* Card utama */} + + + + Detail Posyandu + - - - - Nama Posyandu - {data.name || '-'} - + + + + Nama Posyandu + {data.name || '-'} + - - Nomor Posyandu - {data.nomor || '-'} - + + Nomor Posyandu + {data.nomor || '-'} + - - Deskripsi - - + + Deskripsi + + - - Jadwal Pelayanan - - + + Jadwal Pelayanan + + - - Gambar - {data.image?.link ? ( - {data.name - ) : ( - Tidak ada gambar - )} - + + Gambar + {data.image?.link ? ( + {data.name + ) : ( + Tidak ada gambar + )} + - {/* Aksi */} - - - - + {/* Aksi */} + + - - - - - - - - + + + + + + - {/* Modal konfirmasi hapus */} - setModalHapus(false)} - onConfirm={handleHapus} - text="Apakah Anda yakin ingin menghapus posyandu ini?" - /> - - ); + {/* Modal konfirmasi hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah Anda yakin ingin menghapus posyandu ini?" + /> + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx index ad5047be..606b21d6 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { - Box, - Button, - Group, - Image, - Paper, - Stack, - Text, - TextInput, - Title, - Tooltip, + ActionIcon, + Box, + Button, + Group, + Image, + Loader, + Paper, + Stack, + Text, + TextInput, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -24,191 +25,215 @@ import posyandustate from '../../../_state/kesehatan/posyandu/posyandu'; function CreatePosyandu() { - const statePosyandu = useProxy(posyandustate); - const router = useRouter(); - const [file, setFile] = useState(null); - const [previewImage, setPreviewImage] = useState(null); + const statePosyandu = useProxy(posyandustate); + const router = useRouter(); + const [file, setFile] = useState(null); + const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); - const resetForm = () => { - statePosyandu.create.form = { - name: '', - nomor: '', - deskripsi: '', - imageId: '', - jadwalPelayanan: '', - }; - setFile(null); - setPreviewImage(null); - }; + const resetForm = () => { + statePosyandu.create.form = { + name: '', + nomor: '', + deskripsi: '', + imageId: '', + jadwalPelayanan: '', + }; + setFile(null); + setPreviewImage(null); + }; - const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); - } + const handleSubmit = async () => { + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + // Upload gambar dulu + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + statePosyandu.create.form.imageId = uploaded.id; + await statePosyandu.create.create(); + resetForm(); + router.push('/admin/kesehatan/posyandu'); + } catch (error) { + console.error('Error creating posyandu:', error); + toast.error('Gagal menambahkan posyandu'); + } finally { + setIsSubmitting(false); + } + }; + return ( + + {/* Header */} + + + + Tambah Posyandu + + - // Upload gambar dulu - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); + + + {/* 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/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file (maks 5MB) + + - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } + {previewImage && ( + + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + - statePosyandu.create.form.imageId = uploaded.id; + {/* Input Form */} + (statePosyandu.create.form.name = e.target.value)} + required + /> + (statePosyandu.create.form.nomor = e.target.value)} + required + /> + + + Deskripsi Posyandu + + { + statePosyandu.create.form.deskripsi = htmlContent; + }} + /> + + + + Jadwal Pelayanan + + { + statePosyandu.create.form.jadwalPelayanan = htmlContent; + }} + /> + - await statePosyandu.create.create(); + {/* Button */} + + - - resetForm(); - router.push('/admin/kesehatan/posyandu'); - }; - - - return ( - - {/* Header */} - - - - - - Tambah Posyandu - - - - - - - {/* 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 (maks 5MB) - - - - - {previewImage && ( - - Preview Gambar - - )} - - - - {/* Input Form */} - (statePosyandu.create.form.name = e.target.value)} - required - /> - (statePosyandu.create.form.nomor = e.target.value)} - required - /> - - - Deskripsi Posyandu - - { - statePosyandu.create.form.deskripsi = htmlContent; - }} - /> - - - - Jadwal Pelayanan - - { - statePosyandu.create.form.jadwalPelayanan = htmlContent; - }} - /> - - - - {/* Button */} - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx index b8528787..85691f00 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx @@ -1,23 +1,22 @@ 'use client' import colors from '@/con/colors'; import { - Box, - Button, - Center, - Group, - Pagination, - Paper, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -29,148 +28,146 @@ import posyandustate from '../../_state/kesehatan/posyandu/posyandu'; function Posyandu() { - const [search, setSearch] = useState(""); - return ( - - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - ); + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); } function ListPosyandu({ search }: { search: string }) { - const statePosyandu = useProxy(posyandustate) - const router = useRouter(); + const statePosyandu = useProxy(posyandustate) + const router = useRouter(); - const { - data, - page, - totalPages, - loading, - load, - } = statePosyandu.findMany; + const { + data, + page, + totalPages, + loading, + load, + } = statePosyandu.findMany; - useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + useShallowEffect(() => { + load(page, 10, search) + }, [page, search]) - const filteredData = data || []; + const filteredData = data || []; - if (loading || !data) { - return ( - - - - ) - } + if (loading || !data) { + return ( + + + + ) + } - return ( - - - - Daftar Posyandu - - - - - - - - - Nama Posyandu - Nomor Posyandu - Deskripsi - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - - {item.name} - - - - - - - {item.nomor || '-'} - - - - - - - - - - - - - )) - ) : ( - - -
- Tidak ada data posyandu yang cocok -
-
-
- )} -
-
-
-
-
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
- ); + return ( + + + + Daftar Posyandu + + + + + + + Nama Posyandu + Nomor Posyandu + Deskripsi + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + {item.name} + + + + + + + {item.nomor || '-'} + + + + + + + + + + + + + )) + ) : ( + + +
+ Tidak ada data posyandu yang cocok +
+
+
+ )} +
+
+
+
+
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+
+ ); } 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..ec68731d 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,20 +1,21 @@ /* 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'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -30,86 +31,110 @@ function EditProgramKesehatan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); 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: '', + }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsiSingkat: '', + deskripsi: '', + imageId: '', + imageUrl: '' }); + // 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 || '', + }); + setOriginalData({ + name: data.name || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); + + 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 })); + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsiSingkat: originalData.deskripsiSingkat, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + // Submit form const handleSubmit = async () => { try { - programKesehatanState.edit.form = { - ...programKesehatanState.edit.form, - name: formData.name, - deskripsiSingkat: formData.deskripsiSingkat, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - }; + setIsSubmitting(true); + 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'); + } finally { + setIsSubmitting(false); } }; return ( - {/* Header dengan tombol back */} - - - + Edit Program Kesehatan - {/* Card Form */} - setFormData({ ...formData, name: e.target.value })} - label="Judul" - placeholder="Masukkan judul" - required - /> + {[ + { label: 'Judul', key: 'name', placeholder: 'Masukkan judul' }, + ].map((field) => ( + handleChange(field.key as keyof typeof formData, e.target.value)} + required + /> + ))} - setFormData({ ...formData, deskripsiSingkat: e.target.value })} - label="Deskripsi Singkat" - placeholder="Masukkan deskripsi singkat" - required - /> + + + Deskripsi Singkat + + handleChange('deskripsiSingkat', val)} + /> + @@ -141,7 +173,7 @@ function EditProgramKesehatan() { setFormData({ ...formData, deskripsi: val })} + onChange={(val) => handleChange('deskripsi', val)} /> @@ -152,14 +184,13 @@ function EditProgramKesehatan() { { const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } + if (!selectedFile) return; + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -184,7 +215,7 @@ function EditProgramKesehatan() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx index aae520f5..b1a1e6ec 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx @@ -1,13 +1,13 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip, Image } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; function DetailProgramKesehatan() { const state = useProxy(programKesehatan); @@ -73,50 +73,46 @@ function DetailProgramKesehatan() { Deskripsi Singkat - {data?.deskripsiSingkat || '-'} + Deskripsi - + Gambar {data?.image?.link ? ( - gambar program kesehatan + gambar program kesehatan ) : ( - )} - - - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx index b98f19ed..020c8f50 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -20,13 +22,13 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; -import { Dropzone } from '@mantine/dropzone'; function CreateProgramKesehatan() { const router = useRouter(); const programKesehatanState = useProxy(programKesehatan); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { programKesehatanState.create.form = { @@ -46,36 +48,42 @@ function CreateProgramKesehatan() { if (!programKesehatanState.create.form.deskripsiSingkat) { return toast.warn("Deskripsi singkat wajib diisi"); } - if (!file) { - return toast.warn("Pilih file gambar terlebih dahulu"); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error("Gagal upload gambar"); + } + + programKesehatanState.create.form.imageId = uploaded.id; + await programKesehatanState.create.create(); + + resetForm(); + router.push("/admin/kesehatan/program-kesehatan"); + } catch (error) { + console.error("Error creating program kesehatan:", error); + toast.error("Gagal menambahkan program kesehatan"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } - - programKesehatanState.create.form.imageId = uploaded.id; - await programKesehatanState.create.create(); - - resetForm(); - router.push("/admin/kesehatan/program-kesehatan"); }; return ( {/* Header */} - - - + Tambah Program Kesehatan @@ -101,15 +109,17 @@ function CreateProgramKesehatan() { required /> - { - programKesehatanState.create.form.deskripsiSingkat = val.target.value; - }} - label="Deskripsi Singkat" - placeholder="Masukkan deskripsi singkat" - required - /> + + + Deskripsi Singkat + + { + programKesehatanState.create.form.deskripsiSingkat = val; + }} + /> + @@ -137,7 +147,7 @@ function CreateProgramKesehatan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Dropzone.Accept> @@ -162,7 +172,7 @@ function CreateProgramKesehatan() { </Dropzone> {previewImage && ( - <Box mt="sm"> + <Box mt="sm" pos={"relative"}> <Image src={previewImage} alt="Preview" @@ -175,11 +185,40 @@ function CreateProgramKesehatan() { }} loading="lazy" /> + <ActionIcon + variant="filled" + color="red" + radius="xl" + size="sm" + pos="absolute" + top={5} + right={5} + onClick={() => { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + <IconX size={14} /> + </ActionIcon> </Box> )} </Box> <Group justify="right"> + <Button + variant="outline" + color="gray" + radius="md" + size="md" + onClick={resetForm} + > + Reset + </Button> + + {/* Tombol Simpan */} <Button onClick={handleSubmit} radius="md" @@ -190,7 +229,7 @@ function CreateProgramKesehatan() { boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} > - Simpan + {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'} </Button> </Group> </Stack> diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx index 55ce2a2b..c033c538 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -70,16 +69,14 @@ function ListProgramKesehatan({ search }: { search: string }) { {/* Header List + Tombol Tambah */} <Group justify="space-between" mb="md"> <Title order={4}>Daftar Program Kesehatan - - - + {/* Tabel */} diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx index 6b37fb05..4b3a4350 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' @@ -5,16 +6,17 @@ import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/p import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -53,21 +55,21 @@ function EditPuskesmas() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ - name: statePuskesmas.edit.form.name || '', - alamat: statePuskesmas.edit.form.alamat || '', - jam: { - workDays: statePuskesmas.edit.form.jam?.workDays || '', - weekDays: statePuskesmas.edit.form.jam?.weekDays || '', - holiday: statePuskesmas.edit.form.jam?.holiday || '', - }, - kontak: { - kontakPuskesmas: statePuskesmas.edit.form.kontak?.kontakPuskesmas || '', - email: statePuskesmas.edit.form.kontak?.email || '', - facebook: statePuskesmas.edit.form.kontak?.facebook || '', - kontakUGD: statePuskesmas.edit.form.kontak?.kontakUGD || '', - }, - imageId: statePuskesmas.edit.form.imageId || '', + name: '', + alamat: '', + jam: { workDays: '', weekDays: '', holiday: '' }, + kontak: { kontakPuskesmas: '', email: '', facebook: '', kontakUGD: '' }, + imageId: '', + }); + const [originalData, setOriginalData] = useState({ + name: '', + alamat: '', + jam: { workDays: '', weekDays: '', holiday: '' }, + kontak: { kontakPuskesmas: '', email: '', facebook: '', kontakUGD: '' }, + imageId: '', + imageUrl: '' }); useEffect(() => { @@ -95,6 +97,24 @@ function EditPuskesmas() { }, imageId: form.imageId, }); + setOriginalData({ + name: form.name, + alamat: form.alamat, + jam: { + workDays: form.jam.workDays, + weekDays: form.jam.weekDays, + holiday: form.jam.holiday, + }, + kontak: { + kontakPuskesmas: form.kontak.kontakPuskesmas, + email: form.kontak.email, + facebook: form.kontak.facebook, + kontakUGD: form.kontak.kontakUGD, + }, + imageId: form.imageId, + imageUrl: (form as any).image?.link + + }); const formWithImage = form as PuskesmasFormData; if (formWithImage.image?.link) { @@ -109,8 +129,31 @@ function EditPuskesmas() { loadPuskesmas(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + alamat: originalData.alamat, + jam: { + workDays: originalData.jam.workDays, + weekDays: originalData.jam.weekDays, + holiday: originalData.jam.holiday, + }, + kontak: { + kontakPuskesmas: originalData.kontak.kontakPuskesmas, + email: originalData.kontak.email, + facebook: originalData.kontak.facebook, + kontakUGD: originalData.kontak.kontakUGD, + }, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); statePuskesmas.edit.form = { ...statePuskesmas.edit.form, name: formData.name, @@ -140,6 +183,8 @@ function EditPuskesmas() { } catch (error) { console.error("Error updating puskesmas:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data puskesmas"); + } finally { + setIsSubmitting(false); } }; @@ -159,11 +204,9 @@ function EditPuskesmas() { {/* Header dengan tombol back */} - - - + Edit Puskesmas @@ -264,7 +307,7 @@ function EditPuskesmas() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -288,7 +331,7 @@ function EditPuskesmas() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx index e2f8bef6..d9947978 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx @@ -1,13 +1,13 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas'; function DetailPuskesmas() { const params = useParams(); @@ -73,7 +73,7 @@ function DetailPuskesmas() { Alamat - {data?.alamat || '-'} + {data?.alamat || '-'} @@ -106,7 +106,6 @@ function DetailPuskesmas() { - - - - diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx index ff69d499..62cdc64a 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -26,6 +27,7 @@ function CreatePuskesmas() { const router = useRouter(); const [file, setFile] = useState(null); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { statePuskesmas.create.form = { @@ -51,37 +53,43 @@ function CreatePuskesmas() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + statePuskesmas.create.form.imageId = uploaded.id; + await statePuskesmas.create.submit(); + + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/puskesmas'); + } catch (error) { + console.error('Error creating posyandu:', error); + toast.error('Terjadi kesalahan saat membuat posyandu'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - statePuskesmas.create.form.imageId = uploaded.id; - await statePuskesmas.create.submit(); - - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/puskesmas'); }; return ( {/* Header */} - - - + Tambah Data Puskesmas @@ -171,7 +179,7 @@ function CreatePuskesmas() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -196,7 +204,7 @@ function CreatePuskesmas() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Action Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx index 44e4b74c..0ec25757 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx @@ -16,8 +16,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; @@ -71,7 +70,6 @@ function ListPuskesmas({ search }: { search: string }) { Daftar Puskesmas - - diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx similarity index 54% rename from src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx rename to src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx index b3003d71..c1adc0ac 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx @@ -5,56 +5,71 @@ import sdgsDesa from "@/app/admin/(dashboard)/_state/landing-page/sdgs-desa"; import colors from "@/con/colors"; import ApiFetch from "@/lib/api-fetch"; import { + ActionIcon, Box, Button, Group, + Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, - Image + Title } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; -import { IconArrowBack, IconDeviceFloppy, 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"; import { useProxy } from "valtio/utils"; -function EditKolaborasiInovasi() { +export default function EditKolaborasiInovasi() { const sdgsState = useProxy(sdgsDesa); const router = useRouter(); const params = useParams(); - 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 || '' + name: "", + jumlah: "", + imageId: "", }); - // Load sdgs desa by id saat pertama kali + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + jumlah: "", + imageId: "", + imageUrl: "", + }); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + + // 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 || '', + // isi form awal + const newForm = { + name: data.name || "", + jumlah: data.jumlah || "", + imageId: data.imageId || "", + }; + setFormData(newForm); + + // simpan juga versi original + setOriginalData({ + ...newForm, + imageUrl: data.image?.link || "", }); - if (data.image) { - if (data?.image?.link) { - setPreviewImage(data.image.link); - } - } + + setPreviewImage(data.image?.link || null); } } catch (error) { console.error("Error loading sdgs desa:", error); @@ -65,64 +80,71 @@ function EditKolaborasiInovasi() { loadKolaborasi(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + jumlah: originalData.jumlah, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + 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 - }; + setIsSubmitting(true); + 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"); + router.push("/admin/landing-page/SDGs"); } catch (error) { console.error("Error updating sdgs desa:", error); toast.error("Terjadi kesalahan saat memperbarui sdgs desa"); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Sdgs Desa - Gambar Sdgs Desa + Gambar Program Inovasi { @@ -134,7 +156,7 @@ function EditKolaborasiInovasi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -153,30 +175,54 @@ function EditKolaborasiInovasi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + {/* ✅ Preview gambar + tombol X */} {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} - setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleInputChange("name", e.target.value)} required /> @@ -184,16 +230,26 @@ 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" /> + {/* Tombol Batal */} + + + {/* Tombol Simpan */} @@ -210,5 +266,3 @@ function EditKolaborasiInovasi() { ); } - -export default EditKolaborasiInovasi; diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx similarity index 92% rename from src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx rename to src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx index 59b8f825..3a754953 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import colors from '@/con/colors'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; @@ -27,7 +27,7 @@ function DetailSDGSDesa() { sdgsState.delete.byId(selectedId) setModalHapus(false) setSelectedId(null) - router.push("/admin/landing-page/sdgs-desa") + router.push("/admin/landing-page/SDGs") } } @@ -95,7 +95,6 @@ function DetailSDGSDesa() { - - - - diff --git a/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx new file mode 100644 index 00000000..3364be76 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx @@ -0,0 +1,221 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Loader, ActionIcon } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import sdgsDesa from '../../../_state/landing-page/sdgs-desa'; + + +function CreateSDGsDesa() { + const router = useRouter(); + const stateSDGSDesa = useProxy(sdgsDesa) + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + + useEffect(() => { + stateSDGSDesa.findMany.load(); + }, []); + + const resetForm = () => { + stateSDGSDesa.create.form = { + name: "", + jumlah: "", + imageId: "", + }; + setFile(null); + setPreviewImage(null); + }; + const handleSubmit = async () => { + try { + setIsSubmitting(true); + if (!file) { + return toast.warn("Pilih file image terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }) + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error("Gagal mengupload file"); + } + + stateSDGSDesa.create.form.imageId = uploaded.id; + + await stateSDGSDesa.create.create(); + + resetForm(); + router.push("/admin/landing-page/SDGs") + } catch (error) { + console.error(error); + toast.error("Gagal menambahkan sdgs desa") + } finally { + setIsSubmitting(false); + } + } + return ( + + + + + Tambah Sdgs Desa + + + + + + + + Gambar Program Inovasi + + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + + + + + + {/* ✅ Preview gambar + tombol X */} + {previewImage && ( + + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + + { + stateSDGSDesa.create.form.name = e.currentTarget.value; + }} + required + /> + + + Jumlah + + } + placeholder="Masukkan jumlah" + value={stateSDGSDesa.create.form.jumlah} + onChange={(val) => { + stateSDGSDesa.create.form.jumlah = val.target.value; + }} + required + min={0} + radius="md" + /> + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default CreateSDGsDesa; diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/page.tsx similarity index 92% rename from src/app/admin/(dashboard)/landing-page/sdgs-desa/page.tsx rename to src/app/admin/(dashboard)/landing-page/SDGs/page.tsx index e7eadb5e..5d4eb010 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -59,16 +59,14 @@ function ListSdgsDesa({ search }: { search: string }) { Daftar Sdgs Desa - - @@ -98,13 +96,11 @@ function ListSdgsDesa({ search }: { search: string }) { Daftar Sdgs Desa - -
@@ -129,18 +125,16 @@ function ListSdgsDesa({ search }: { search: string }) { - - ))} diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx index c9ed189e..a7914073 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx @@ -1,134 +1,221 @@ /* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + 'use client'; import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, + Badge, Box, Button, Group, Image, + Loader, + NumberInput, Paper, + Select, Stack, + Table, Text, TextInput, Title, - Tooltip, } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { + IconArrowBack, + IconFile, + IconPhoto, + IconPlus, + IconTrash, + IconX +} from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +// Tipe untuk form item +type ItemForm = { + kode: string; + uraian: string; + anggaran: number; + realisasi: number; + level: number; + tipe: 'pendapatan' | 'belanja' | 'pembiayaan'; +}; + function EditAPBDes() { const apbdesState = useProxy(apbdes); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [previewImage, setPreviewImage] = useState(null); const [previewDoc, setPreviewDoc] = useState(null); const [imageFile, setImageFile] = useState(null); const [docFile, setDocFile] = useState(null); - const [formData, setFormData] = useState({ - name: apbdesState.edit.form.name || '', - jumlah: apbdesState.edit.form.jumlah || '', - imageId: apbdesState.edit.form.imageId || '', - fileId: apbdesState.edit.form.fileId || '' + + // Form input untuk item baru + const [newItem, setNewItem] = useState({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', }); - // Load APBDes data by id - useEffect(() => { - const loadAPBDes = async () => { - const id = params?.id as string; - if (!id) return; - - try { - const data = await apbdesState.edit.load(id); - if (data) { - setFormData({ - name: data.name || '', - jumlah: data.jumlah || '', - imageId: data.imageId || '', - fileId: data.fileId || '' - }); - - if (data.image?.link) setPreviewImage(data.image.link); - if (data.file?.link) setPreviewDoc(data.file.link); - } - } catch (error) { - console.error('Error loading APBDes:', error); - toast.error('Gagal memuat data APBDes'); - } + // Type for the API response + interface APBDesResponse { + id: string; + image?: { + link: string; + id: string; }; + file?: { + link: string; + id: string; + }; + // Add other properties as needed + } - loadAPBDes(); + // Load data saat pertama kali + useEffect(() => { + const id = params?.id as string; + if (id) { + apbdesState.edit.load(id).then((response) => { + const data = response as unknown as APBDesResponse; + if (data) { + // ✅ Ambil link langsung dari response + setPreviewImage(data.image?.link || null); + setPreviewDoc(data.file?.link || null); + } + }); + } }, [params?.id]); + const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => { + const file = files[0]; + if (!file) return; + + if (fileType === 'image') { + setImageFile(file); + setPreviewImage(URL.createObjectURL(file)); + } else { + setDocFile(file); + setPreviewDoc(URL.createObjectURL(file)); + } + }; + + const handleAddItem = () => { + const { kode, uraian, anggaran, realisasi, level, tipe } = newItem; + if (!kode || !uraian) { + return toast.warn('Kode dan uraian wajib diisi'); + } + + const finalTipe = level === 1 ? null : tipe; + const selisih = realisasi - anggaran; + const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; + + apbdesState.edit.addItem({ + kode, + uraian, + anggaran, + realisasi, + selisih, + persentase, + level, + tipe: finalTipe, // ✅ Tidak akan undefined + }); + + + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); + }; + + const handleRemoveItem = (index: number) => { + apbdesState.edit.removeItem(index); + }; + const handleSubmit = async () => { + if (apbdesState.edit.form.items.length === 0) { + return toast.warn('Minimal harus ada 1 item APBDes'); + } + try { - // Update global state with form data - apbdesState.edit.form = { - ...apbdesState.edit.form, - ...formData, - }; + setIsSubmitting(true); - // Upload new image if exists + // Upload file baru jika ada if (imageFile) { - const res = await ApiFetch.api.fileStorage.create.post({ - file: imageFile, - name: imageFile.name + const res = await ApiFetch.api.fileStorage.create.post({ + file: imageFile, + name: imageFile.name, }); - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - apbdesState.edit.form.imageId = uploaded.id; + const imageId = res.data?.data?.id; + if (imageId) apbdesState.edit.form.imageId = imageId; } - // Upload new document if exists if (docFile) { - const res = await ApiFetch.api.fileStorage.create.post({ - file: docFile, - name: docFile.name + const res = await ApiFetch.api.fileStorage.create.post({ + file: docFile, + name: docFile.name, }); - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal upload dokumen'); - } - - apbdesState.edit.form.fileId = uploaded.id; + const fileId = res.data?.data?.id; + if (fileId) apbdesState.edit.form.fileId = fileId; } - await apbdesState.edit.update(); - toast.success('APBDes berhasil diperbarui!'); - router.push('/admin/landing-page/apbdes'); - } catch (error) { - console.error('Error updating APBDes:', error); - toast.error('Terjadi kesalahan saat memperbarui APBDes'); + const success = await apbdesState.edit.update(); + if (success) { + router.push('/admin/landing-page/apbdes'); + } + } catch (err) { + console.error('Update error:', err); + toast.error('Gagal memperbarui APBDes'); + } finally { + setIsSubmitting(false); + } + }; + + const handleReset = () => { + const id = params?.id as string; + if (id) { + apbdesState.edit.load(id); + setImageFile(null); + setDocFile(null); + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); + toast.info('Form dikembalikan ke data awal'); } }; return ( - - - + Edit APBDes - setFormData({ ...formData, name: e.target.value })} + {/* Header Form */} + + (apbdesState.edit.form.tahun = Number(val) || new Date().getFullYear()) + } + min={2000} + max={2100} required /> - setFormData({ ...formData, jumlah: e.target.value })} - required - /> + {/* Gambar & Dokumen */} + + + + Gambar APBDes + + toast.error('File gambar tidak valid')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + {previewImage ? 'Ganti gambar' : 'Unggah gambar'} + + + + + {previewImage && ( + + + { + setPreviewImage(null); + setImageFile(null); + }} + > + + + + )} + - - - Gambar APBDes - - { - const selectedFile = files[0]; - if (selectedFile) { - setImageFile(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 - - - - + + + Dokumen APBDes + + toast.error('File dokumen tidak valid')} + maxSize={10 * 1024 ** 2} + accept={{ + 'application/pdf': ['.pdf'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'application/vnd.ms-excel': ['.xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + }} + radius="md" + p="xl" + > + + + + + + + {previewDoc ? 'Ganti dokumen' : 'Unggah dokumen'} + + + + + {previewDoc && ( + + + { + setPreviewDoc(null); + setDocFile(null); + }} + > + + + + )} + + - {previewImage && ( - - + + Tambah Item Pendapatan/Belanja + + + + setNewItem({ ...newItem, kode: e.target.value })} + required + /> + setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })} /> - - )} - - - - - Dokumen APBDes - - { - const selectedFile = files[0]; - if (selectedFile) { - setDocFile(selectedFile); - setPreviewDoc(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error('File tidak valid, gunakan format dokumen')} - maxSize={10 * 1024 ** 2} // 10MB - accept={{ - 'application/pdf': ['.pdf'], - 'application/msword': ['.doc'], - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], - 'application/vnd.ms-excel': ['.xls'], - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - }} - radius="md" - p="xl" - > - - - - - - - - - - - - - Seret dokumen atau klik untuk memilih file - - - Maksimal 10MB, format PDF/DOC/DOCX/XLS/XLSX - - - + setNewItem({ ...newItem, uraian: e.target.value })} + required + /> + + setNewItem({ ...newItem, anggaran: Number(val) || 0 })} + thousandSeparator + min={0} + /> + setNewItem({ ...newItem, realisasi: Number(val) || 0 })} + thousandSeparator + min={0} + /> + + + + - {previewDoc && ( - - - Dokumen terpilih: {docFile?.name || 'Dokumen'} - - - - )} - + {/* Tabel Items */} + {apbdesState.edit.form.items.length > 0 && ( + + + Daftar Item ({apbdesState.edit.form.items.length}) + +
+ + + + + + + + + + + + + {apbdesState.edit.form.items.map((item, idx) => ( + + + + + + + + + + ))} + +
KodeUraianAnggaranRealisasiLevelTipeAksi
+ + {item.kode} + + {item.uraian}{item.anggaran.toLocaleString('id-ID')}{item.realisasi.toLocaleString('id-ID')} + + L{item.level} + + + {item.tipe ? ( + + {item.tipe} + + ) : ( + '-' + )} + + handleRemoveItem(idx)}> + + +
+
+ )} - + {/* Tombol Aksi */} + + @@ -293,4 +493,4 @@ function EditAPBDes() { ); } -export default EditAPBDes; +export default EditAPBDes; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx index ce342456..a608f8ac 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx @@ -1,36 +1,53 @@ -'use client' +/* eslint-disable react-hooks/exhaustive-deps */ +'use client'; import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { + Box, + Button, + Group, + Image, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text +} from '@mantine/core'; import { IconArrowBack, IconEdit, IconFile, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import colors from '@/con/colors'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import apbdes from '../../../_state/landing-page/apbdes'; + + function DetailAPBDes() { - const apbdesState = useProxy(apbdes) - const [modalHapus, setModalHapus] = useState(false) - const [selectedId, setSelectedId] = useState(null) - const params = useParams() - const router = useRouter() - - useShallowEffect(() => { - apbdesState.findUnique.load(params?.id as string) - }, []) + const apbdesState = useProxy(apbdes); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const params = useParams(); + const router = useRouter(); + useEffect(() => { + if (!params?.id) return; + apbdesState.findUnique.load(params.id as string); + }, [params?.id]); const handleHapus = () => { if (selectedId) { - apbdesState.delete.byId(selectedId) - setModalHapus(false) - setSelectedId(null) - router.push("/admin/landing-page/apbdes") + apbdesState.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push('/admin/landing-page/apbdes'); } - } + }; if (!apbdesState.findUnique.data) { return ( @@ -42,6 +59,11 @@ function DetailAPBDes() { const data = apbdesState.findUnique.data; + // Helper: indentasi berdasarkan level + const getIndent = (level: number) => ({ + paddingLeft: `${(level - 1) * 20}px`, + }); + return ( - + - - - + + + {/* Tabel Items */} + {data.items && data.items.length > 0 ? ( + + + Rincian Pendapatan & Belanja ({data.items.length} item) + + + + + + Uraian + Anggaran (Rp) + Realisasi (Rp) + Selisih (Rp) + Persentase (%) + + + + {[...data.items] // Create a new array before sorting + .sort((a, b) => a.kode.localeCompare(b.kode)) + .map((item) => ( + + + + {item.kode} + {item.uraian} + + + {item.anggaran.toLocaleString('id-ID')} + {item.realisasi.toLocaleString('id-ID')} + + = 0 ? 'green' : 'red'}> + {item.selisih.toLocaleString('id-ID')} + + + + {item.persentase.toFixed(2)}% + + + ))} + +
+
+
+ ) : ( + Belum ada data item + )} 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..f91d6a37 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import colors from '@/con/colors'; @@ -12,82 +13,153 @@ import { Text, TextInput, Title, - Tooltip, + Loader, + ActionIcon, + NumberInput, + Select, + Table, + Badge, } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX, IconPlus, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import apbdes from '../../../_state/landing-page/apbdes'; +// Tipe item untuk form +type ItemForm = { + kode: string; + uraian: string; + anggaran: number; + realisasi: number; + level: number; + tipe: 'pendapatan' | 'belanja' | 'pembiayaan'; +}; function CreateAPBDes() { const router = useRouter(); - const stateAPBDes = useProxy(apbdes) + const stateAPBDes = useProxy(apbdes); const [previewImage, setPreviewImage] = useState(null); const [previewDoc, setPreviewDoc] = useState(null); const [imageFile, setImageFile] = useState(null); const [docFile, setDocFile] = useState(null); - + const [isSubmitting, setIsSubmitting] = useState(false); + // Form sementara untuk input item baru + const [newItem, setNewItem] = useState({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); useEffect(() => { stateAPBDes.findMany.load(); }, []); const resetForm = () => { - stateAPBDes.create.form = { - name: "", - jumlah: "", - imageId: "", - fileId: "", - }; + stateAPBDes.create.reset(); setImageFile(null); setDocFile(null); setPreviewImage(null); + setPreviewDoc(null); + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); }; + const handleSubmit = async () => { if (!imageFile || !docFile) { return toast.warn("Pilih gambar dan dokumen terlebih dahulu"); } - + if (stateAPBDes.create.form.items.length === 0) { + return toast.warn("Minimal tambahkan 1 item APBDes"); + } + try { + setIsSubmitting(true); const [uploadImageRes, uploadDocRes] = await Promise.all([ ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name }), ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name }), ]); - + const imageId = uploadImageRes?.data?.data?.id; const fileId = uploadDocRes?.data?.data?.id; - + if (!imageId || !fileId) { return toast.error("Gagal mengupload file"); } - + + // Update form dengan ID file stateAPBDes.create.form.imageId = imageId; stateAPBDes.create.form.fileId = fileId; - + await stateAPBDes.create.create(); - + toast.success("Berhasil menambahkan APBDes"); resetForm(); router.push("/admin/landing-page/apbdes"); } catch (error) { console.error("Gagal submit:", error); toast.error("Gagal menyimpan data"); + } finally { + setIsSubmitting(false); } }; - + + // Tambahkan item ke state + const handleAddItem = () => { + const { kode, uraian, anggaran, realisasi, level, tipe } = newItem; + if (!kode || !uraian) { + return toast.warn("Kode dan uraian wajib diisi"); + } + + const finalTipe = level === 1 ? null : tipe; + const selisih = realisasi - anggaran; + const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; + + stateAPBDes.create.addItem({ + kode, + uraian, + anggaran, + realisasi, + selisih, + persentase, + level, + tipe: finalTipe, + }); + + // Reset form input + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); + }; + + // Hapus item + const handleRemoveItem = (index: number) => { + stateAPBDes.create.removeItem(index); + }; + return ( - - - + Tambah APBDes @@ -102,147 +174,292 @@ function CreateAPBDes() { style={{ border: '1px solid #e0e0e0' }} > - {/* Gambar APBDes */} - - - Gambar APBDes - - { - const selectedFile = files[0]; - if (selectedFile) { - setImageFile(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: JPEG, JPG, PNG, GIF, WEBP, SVG) - + {/* Gambar & Dokumen (dipendekkan untuk fokus pada items) */} + + {/* Gambar APBDes */} + + + Gambar APBDes + + { + const selectedFile = files[0]; + if (selectedFile) { + setImageFile(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 + + + + + {previewImage && ( + + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setImageFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + - - + )} + - {previewImage && ( - - Preview Gambar - - )} - + {/* Dokumen APBDes */} + + + Dokumen APBDes + + { + const selectedFile = files[0]; + if (selectedFile) { + setDocFile(selectedFile); + setPreviewDoc(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid')} + maxSize={5 * 1024 ** 2} + accept={{ + 'application/pdf': ['.pdf'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret dokumen atau klik untuk memilih + + + + + {previewDoc && ( + + + Pratinjau Dokumen + + + + - + - ) : null} + )} diff --git a/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx b/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx index 47886d21..1ccaa2f0 100644 --- a/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx +++ b/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx @@ -8,10 +8,12 @@ import React, { useState } from 'react'; import { useProxy } from 'valtio/utils'; import BackButton from '../../../desa/layanan/_com/BackButto'; import { useRouter } from 'next/navigation'; +import { useDebouncedValue } from '@mantine/hooks'; function Page() { const state = useProxy(polsekTerdekatState); const [search, setSearch] = useState(''); + const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const router = useRouter() const { @@ -23,8 +25,8 @@ function Page() { } = state.findMany; useShallowEffect(() => { - load(page, 3, search) - }, [page, search]) + load(page, 3, debouncedSearch) + }, [page, debouncedSearch]) if (loading || !data) { return ( diff --git a/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx b/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx index 6adbdfe3..d24e95e6 100644 --- a/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx +++ b/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx @@ -56,8 +56,11 @@ function Page() { /> - - Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. + + Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). + + + Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
@@ -82,7 +85,7 @@ function Page() { {v.judul} - +
diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx index c989a0be..7fe14d54 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx @@ -78,28 +78,25 @@ function Page() { Pendahuluan - - {state.findUnique.data.introduction?.content} - + - Kenali Gejala DBD + {state.findUnique.data.symptom?.title} - {state.findUnique.data.symptom?.title} - + {state.findUnique.data.prevention?.title} - + {state.findUnique.data.firstaid?.title} - + @@ -117,10 +114,10 @@ function Page() { {state.findUnique.data?.mythvsfact ? ( - + - + ) : ( diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx index abcb1889..f38c60a1 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx @@ -1,7 +1,7 @@ 'use client' import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import colors from '@/con/colors'; -import { Anchor, Box, Card, Divider, Group, Image, Loader, Paper, Stack, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Card, Divider, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconCalendar, IconChevronRight } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -28,9 +28,9 @@ function ArtikelKesehatanPage() { - + <Text ta="center" fw={700} fz="32px" c={colors['blue-button']}> Artikel Kesehatan - + {state.findMany.data.length === 0 ? ( @@ -51,31 +51,30 @@ function ArtikelKesehatanPage() { onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')} > - {item.title} + {item.title} - {item.title} + {item.title} - + {new Date(item.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' })} • Dinas Kesehatan - + {item.content} - - + +
)) diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx index 8c1500ab..6aa24dfd 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx @@ -3,9 +3,9 @@ import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; import colors from '@/con/colors'; -import { ActionIcon, Anchor, AspectRatio, Badge, Box, Button, Card, Chip, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core'; +import { ActionIcon, AspectRatio, Badge, Box, Button, Card, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconStethoscope, IconUser, IconUsersGroup, IconWallet } from '@tabler/icons-react'; +import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconUser } from '@tabler/icons-react'; import { useParams } from 'next/navigation'; import { useMemo } from 'react'; import { useProxy } from 'valtio/utils'; @@ -39,10 +39,9 @@ function Page() { const nama = data?.name || 'Fasilitas Kesehatan'; const prosedur = data?.prosedurpendaftaran.content || ''; - console.log("Prosedur:", data?.prosedurpendaftaran); const alamat = data?.informasiumum?.alamat || '-'; const jam = data?.informasiumum?.jamOperasional || '-'; - const layananUnggulan = data?.layananunggulan || ''; + const layananUnggulan = data?.layananunggulan?.content || ''; const tenaga = data?.dokterdantenagamedis || null; const fasilitasPendukungHtml = data?.fasilitaspendukung?.content || ''; const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null; @@ -150,11 +149,6 @@ function Page() { - - }>Layanan Medis - }>Ramah Keluarga - }>Pembayaran Non-Tunai -
@@ -183,7 +177,7 @@ function Page() { Layanan Unggulan {layananUnggulan ? ( - + ) : ( @@ -211,7 +205,6 @@ function Page() { - Kunjungi situs resmi @@ -247,20 +240,13 @@ function Page() { - - - - - - - - + Fasilitas Pendukung {fasilitasPendukungHtml ? ( - + ) : ( @@ -271,8 +257,7 @@ function Page() { )} - - + Layanan & Tarif @@ -310,17 +295,18 @@ function Page() { )} + - + Prosedur Pendaftaran {prosedur ? ( - + ) : ( Belum ada prosedur pendaftaran )} diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/page.tsx index f3043dcc..6c843f0c 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/page.tsx @@ -1,11 +1,11 @@ 'use client' import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import colors from '@/con/colors'; -import { Anchor, Badge, Box, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { Badge, Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; +import { IconChevronRight, IconClock, IconMapPin } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; -import { IconMapPin, IconClock, IconArrowRight } from '@tabler/icons-react'; function FasilitasKesehatanPage() { const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan); @@ -36,72 +36,73 @@ function FasilitasKesehatanPage() { - {state.findMany.data.length === 0 ? ( - - - Belum ada fasilitas kesehatan yang tersedia - - - ) : ( - state.findMany.data.map((item) => ( - { - (e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)'; - (e.currentTarget as HTMLElement).style.boxShadow = '0 8px 20px rgba(0,0,0,0.08)'; - }} - onMouseLeave={(e) => { - (e.currentTarget as HTMLElement).style.transform = 'translateY(0px)'; - (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.05)'; - }} - > - - - - {item.name} - - - Aktif - - - - - - {item.informasiumum.alamat} - - - - - - {item.informasiumum.jamOperasional} - - - - router.push( - `/darmasaba/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/${item.id}` - ) - } - c={colors['blue-button']} - fz="sm" - fw={600} - style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }} - > - Lihat Detail - - - - - )) - )} + {state.findMany.data.length === 0 ? ( + + + Belum ada fasilitas kesehatan yang tersedia + + + ) : ( + state.findMany.data.map((item) => ( + { + (e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)'; + (e.currentTarget as HTMLElement).style.boxShadow = '0 8px 20px rgba(0,0,0,0.08)'; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLElement).style.transform = 'translateY(0px)'; + (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.05)'; + }} + > + + + + {item.name} + + + Aktif + + + + + + {item.informasiumum.alamat} + + + + + + {item.informasiumum.jamOperasional} + + + + + + + + )) + )} diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/[id]/page.tsx index ac54f30d..f5af70e8 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/[id]/page.tsx @@ -4,14 +4,16 @@ import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; import colors from '@/con/colors'; import { Box, + Button, Divider, Group, + Modal, Paper, Skeleton, Stack, Text } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDisclosure, useShallowEffect } from '@mantine/hooks'; import { IconMail, IconPhone, IconUser } from '@tabler/icons-react'; import { useParams } from 'next/navigation'; import { useProxy } from 'valtio/utils'; @@ -21,6 +23,7 @@ import CreatePendaftaran from '../create/page'; function Page() { const params = useParams(); const state = useProxy(jadwalkegiatanState); + const [opened, { open, close }] = useDisclosure(false); useShallowEffect(() => { state.findUnique.load(params?.id as string); @@ -66,28 +69,38 @@ function Page() { Deskripsi Kegiatan - + Layanan yang Tersedia - + Syarat & Ketentuan - + Dokumen yang Perlu Dibawa - + - + + Pendaftaran Kegiatan + + + + + + + + + diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/page.tsx index da57f97c..c7bcc1e9 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/page.tsx @@ -49,29 +49,33 @@ function JadwalKegiatanPage() { > - + {item.informasijadwalkegiatan.name} - {item.informasijadwalkegiatan.tanggal} + {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric' + })} - + {item.informasijadwalkegiatan.waktu} - + {item.informasijadwalkegiatan.lokasi} - + diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/page.tsx index 31c1ee63..7000f374 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/page.tsx @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import colors from '@/con/colors'; import { BarChart as MantineBarChart } from '@mantine/charts'; -import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; +import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core'; import { useEffect, useState } from 'react'; import BackButton from '../../desa/layanan/_com/BackButto'; @@ -107,20 +107,7 @@ function Page() { - - - - Angka Kematian - - - - - - Angka Kelahiran - - - - + Data Kematian dan Kelahiran {chartData.length === 0 ? ( Belum ada data yang tersedia untuk ditampilkan @@ -150,6 +137,20 @@ function Page() { )} + + + + Angka Kematian + + + + + + Angka Kelahiran + + + + @@ -163,11 +164,11 @@ function Page() { }} > {/* Fasilitas Kesehatan */} - + {/* Jadwal Kegiatan */} - + {/* Artikel Kesehatan */} - + diff --git a/src/app/darmasaba/(pages)/kesehatan/info-wabah-penyakit/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/info-wabah-penyakit/[id]/page.tsx new file mode 100644 index 00000000..cb19a507 --- /dev/null +++ b/src/app/darmasaba/(pages)/kesehatan/info-wabah-penyakit/[id]/page.tsx @@ -0,0 +1,86 @@ +'use client' +import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; +import colors from '@/con/colors'; +import { Box, Button, Image, Paper, Skeleton, Stack, Text } 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 DetailInfoWabahPenyakitUser() { + const state = useProxy(infoWabahPenyakit); + const router = useRouter(); + const params = useParams(); + + useShallowEffect(() => { + state.findUnique.load(params?.id as string); + }, []); + + if (!state.findUnique.data) { + return ( + + + + ); + } + + const data = state.findUnique.data; + + return ( + + {/* Tombol Back */} + + + + + {/* Wrapper Detail */} + + + {/* Judul */} + + {data.name || 'Kontak Darurat'} + + + {/* Gambar */} + {data.image?.link && ( + {data.name} + )} + + {/* Deskripsi */} + + Deskripsi + + + + + + ); +} + +export default DetailInfoWabahPenyakitUser; diff --git a/src/app/darmasaba/(pages)/kesehatan/info-wabah-penyakit/page.tsx b/src/app/darmasaba/(pages)/kesehatan/info-wabah-penyakit/page.tsx index 0ac6874c..e65feec2 100644 --- a/src/app/darmasaba/(pages)/kesehatan/info-wabah-penyakit/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/info-wabah-penyakit/page.tsx @@ -2,10 +2,14 @@ import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import colors from '@/con/colors'; import { + Badge, Box, + Button, Center, + Divider, Grid, GridCol, + Group, Image, Pagination, Paper, @@ -13,27 +17,25 @@ import { Skeleton, Stack, Text, - TextInput, - Badge, - HoverCard, - Divider, - Group, + TextInput } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconSearch, IconInfoCircle } from '@tabler/icons-react'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconInfoCircle, IconSearch } from '@tabler/icons-react'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; +import { useTransitionRouter } from 'next-view-transitions'; function Page() { const state = useProxy(infoWabahPenyakit); + const router = useTransitionRouter(); const [search, setSearch] = useState(''); - + const [debouncedSearch] = useDebouncedValue(search, 500) const { data, page, totalPages, loading, load } = state.findMany; useShallowEffect(() => { - load(page, 3, search); - }, [page, search]); + load(page, 3, debouncedSearch); + }, [page, debouncedSearch]); if (loading || !data) { return ( @@ -59,7 +61,7 @@ function Page() { > Informasi Wabah & Penyakit - + Dapatkan informasi terbaru mengenai wabah dan penyakit yang sedang diawasi. @@ -82,7 +84,7 @@ function Page() {
- + Tidak ada data yang cocok dengan pencarian Anda. @@ -99,17 +101,35 @@ function Page() { bg={colors['white-trans-1']} style={{ transition: 'transform 200ms ease, box-shadow 200ms ease', + display: 'flex', + flexDirection: 'column', }} > - - + {/* Gambar */} + + w="100%" + style={{ + overflow: 'hidden', + borderRadius: '8px', + }} + > + {v.name} + + + {/* Judul dan badge */} {v.name} @@ -118,52 +138,60 @@ function Page() { Wabah + - Diposting: 12 Februari 2025 · Dinas Kesehatan + Diposting:{' '} + {new Date(v.createdAt).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + - - {v.deskripsiSingkat} - - - - - Lihat detail lengkap - - - - - - + + {/* Bagian deskripsi dan tombol */} + + + + + + ))} + )} - -
- load(newPage)} - total={totalPages} - radius="xl" - size="md" - mt="lg" - /> -
+ +
+ load(newPage)} + total={totalPages} + radius="xl" + size="md" + mt="lg" + /> +
); diff --git a/src/app/darmasaba/(pages)/kesehatan/kontak-darurat/page.tsx b/src/app/darmasaba/(pages)/kesehatan/kontak-darurat/page.tsx index aa19062a..0e7913f3 100644 --- a/src/app/darmasaba/(pages)/kesehatan/kontak-darurat/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/kontak-darurat/page.tsx @@ -1,9 +1,9 @@ 'use client' -import { useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; +import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; +import colors from '@/con/colors'; import { Box, + Button, Center, Grid, GridCol, @@ -15,23 +15,24 @@ import { Stack, Text, TextInput, - Tooltip, - Badge, + Tooltip } from '@mantine/core'; -import { IconSearch, IconPhone } from '@tabler/icons-react'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconBrandWhatsapp, IconSearch } from '@tabler/icons-react'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; -import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; -import colors from '@/con/colors'; +import { useDebouncedValue } from '@mantine/hooks'; function Page() { const state = useProxy(kontakDarurat); const [search, setSearch] = useState(''); - + const [debouncedSearch] = useDebouncedValue(search, 500) const { data, page, totalPages, loading, load } = state.findMany; useShallowEffect(() => { - load(page, 6, search); - }, [page, search]); + load(page, 3, debouncedSearch); + }, [page, debouncedSearch]); if (loading || !data) { return ( @@ -52,7 +53,7 @@ function Page() { Kontak Darurat - + Hubungi layanan penting dengan cepat dan mudah @@ -87,65 +88,102 @@ function Page() { ) : ( {data.map((v, k) => ( - - - {v.name} - - {v.name} - - - - - } - variant="light" - mt="sm" - > - Panggil Sekarang - - - + + + + {v.name} (e.currentTarget.style.transform = 'scale(1.05)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')} + /> + + + + {v.name} + + + + + + + + {/* ✅ Tombol selalu di bagian bawah card */} +
+ +
+
+ + ))}
)} - {totalPages > 1 && ( -
- load(newPage, 6, search)} - total={totalPages} - radius="xl" - size="md" - styles={{ - control: { - borderRadius: '999px', - }, - }} - /> -
- )} +
+ load(newPage, 3, search)} + total={totalPages} + size="lg" + radius="xl" + styles={{ + control: { + border: `1px solid ${colors['blue-button']}`, + }, + }} + /> +
); } diff --git a/src/app/darmasaba/(pages)/kesehatan/penanganan-darurat/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/penanganan-darurat/[id]/page.tsx new file mode 100644 index 00000000..633bff4b --- /dev/null +++ b/src/app/darmasaba/(pages)/kesehatan/penanganan-darurat/[id]/page.tsx @@ -0,0 +1,78 @@ +'use client'; +import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'; +import colors from '@/con/colors'; +import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { useParams } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; +import BackButton from '../../../desa/layanan/_com/BackButto'; + +function DetailPenangananDaruratUser() { + const state = useProxy(penangananDarurat); + const params = useParams(); + + useShallowEffect(() => { + state.findUnique.load(params?.id as string); + }, []); + + if (!state.findUnique.data) { + return ( + + + + + + ); + } + + const data = state.findUnique.data; + + return ( + + {/* Tombol Back */} + + + + + {/* Wrapper Detail */} + + + + {data.name || 'Penanganan Darurat'} + + + {data.image?.link && ( + {data.name} + )} + + + + + + + + ); +} + +export default DetailPenangananDaruratUser; diff --git a/src/app/darmasaba/(pages)/kesehatan/penanganan-darurat/page.tsx b/src/app/darmasaba/(pages)/kesehatan/penanganan-darurat/page.tsx index 17b83e42..d08080c8 100644 --- a/src/app/darmasaba/(pages)/kesehatan/penanganan-darurat/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/penanganan-darurat/page.tsx @@ -2,8 +2,8 @@ import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat' import colors from '@/con/colors' import { - Badge, Box, + Button, Center, Grid, GridCol, @@ -17,7 +17,7 @@ import { TextInput, Tooltip } from '@mantine/core' -import { useShallowEffect } from '@mantine/hooks' +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks' import { IconSearch } from '@tabler/icons-react' import { useState } from 'react' import { useProxy } from 'valtio/utils' @@ -26,12 +26,12 @@ import BackButton from '../../desa/layanan/_com/BackButto' function Page() { const state = useProxy(penangananDarurat) const [search, setSearch] = useState('') - + const [debouncedSearch] = useDebouncedValue(search, 500) const { data, page, totalPages, loading, load } = state.findMany useShallowEffect(() => { - load(page, 6, search) - }, [page, search]) + load(page, 3, debouncedSearch) + }, [page, debouncedSearch]) if (loading || !data) { return ( @@ -52,7 +52,7 @@ function Page() { Penanganan Darurat - + Informasi cepat dan jelas untuk situasi darurat kesehatan @@ -97,20 +97,36 @@ function Page() { shadow="sm" withBorder bg={colors['white-trans-1']} - style={{ transition: 'all 0.3s ease' }} + style={{ + transition: 'all 0.3s ease', + transform: 'translateY(0)', + }} + onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-5px)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')} > -
+ {v.name} -
+ + + - - Darurat -
))} @@ -140,22 +163,24 @@ function Page() { )} - {totalPages > 1 && ( -
- load(newPage, 6, search)} - total={totalPages} - size="lg" - radius="xl" - styles={{ - control: { - border: `1px solid ${colors['blue-button']}`, - }, - }} - /> -
- )} +
+ load(newPage, 3, search)} + total={totalPages} + size="lg" + radius="xl" + styles={{ + control: { + border: `1px solid ${colors['blue-button']}`, + transition: 'all 0.3s ease', + '&:hover': { backgroundColor: colors['blue-button'], color: 'white' }, + }, + }} + + /> +
+ ) } diff --git a/src/app/darmasaba/(pages)/kesehatan/posyandu/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/posyandu/[id]/page.tsx new file mode 100644 index 00000000..1cf5b958 --- /dev/null +++ b/src/app/darmasaba/(pages)/kesehatan/posyandu/[id]/page.tsx @@ -0,0 +1,121 @@ +'use client'; + +import colors from '@/con/colors'; +import { Button, Center, Flex, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; +import posyanduState from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu'; + +export default function DetailPosyanduUser() { + const statePosyandu = useProxy(posyanduState); + const params = useParams(); + const router = useRouter(); + + useShallowEffect(() => { + statePosyandu.findUnique.load(params?.id as string); + }, []); + + if (!statePosyandu.findUnique.data) { + return ( + + + + ); + } + + const data = statePosyandu.findUnique.data; + + return ( + + {/* Tombol Kembali */} + + + + + {/* Konten utama */} + + + {/* Header */} + + {data.name || 'Posyandu Desa'} + + + {/* Gambar */} + {data.image?.link ? ( +
+ {`Gambar +
+ ) : ( +
+ + Tidak ada gambar + +
+ )} + + {/* Info utama */} + + + + + {data.nomor || 'Nomor tidak tersedia'} + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/src/app/darmasaba/(pages)/kesehatan/posyandu/page.tsx b/src/app/darmasaba/(pages)/kesehatan/posyandu/page.tsx index 4f80a440..1048dcb2 100644 --- a/src/app/darmasaba/(pages)/kesehatan/posyandu/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/posyandu/page.tsx @@ -1,22 +1,25 @@ 'use client' import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu"; import colors from "@/con/colors"; -import { Badge, Box, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core"; -import { useShallowEffect } from "@mantine/hooks"; +import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core"; +import { useDebouncedValue, useShallowEffect } from "@mantine/hooks"; import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react"; import { useState } from "react"; import { useProxy } from "valtio/utils"; import BackButton from "../../desa/layanan/_com/BackButto"; +import { useTransitionRouter } from "next-view-transitions"; export default function Page() { const state = useProxy(posyandustate); const [search, setSearch] = useState(""); + const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay + const router = useTransitionRouter() const { data, page, totalPages, loading, load } = state.findMany; useShallowEffect(() => { - load(page, 6, search); - }, [page, search]); + load(page, 6, debouncedSearch); + }, [page, debouncedSearch]); if (loading || !data) { return ( @@ -26,6 +29,36 @@ export default function Page() { ); } + if (data.length === 0) { + return ( + + + + + + Posyandu Desa Darmasaba + + } + w={{ base: "100%", md: "35%" }} + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + + ); + } + return ( @@ -101,31 +134,41 @@ export default function Page() { loading="lazy" />
- - - - {v.nomor || "Tidak tersedia"} - + + + + + {v.nomor || "Tidak tersedia"} + + - - - - Jadwal:{" "} - - + + + + + + Jadwal:{" "} + + + - + + + - + + ))} diff --git a/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/[id]/page.tsx index e40a6088..c60b38cc 100644 --- a/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/[id]/page.tsx @@ -28,10 +28,11 @@ function Page() { } return ( - + + - + {state.findUnique.data.createdAt @@ -83,13 +85,14 @@ function Page() { - + Admin Desa + ); } diff --git a/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/page.tsx b/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/page.tsx index 6b58c9d6..ed5a360f 100644 --- a/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/page.tsx @@ -1,4 +1,5 @@ 'use client' +import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan"; import colors from "@/con/colors"; import { Box, @@ -15,9 +16,9 @@ import { Stack, Text, TextInput, - Tooltip, - Transition, + Transition } from "@mantine/core"; +import { useDebouncedValue, useShallowEffect } from "@mantine/hooks"; import { IconBarbell, IconCalendar, @@ -26,12 +27,10 @@ import { IconUser, IconUsersGroup, } from "@tabler/icons-react"; -import BackButton from "../../desa/layanan/_com/BackButto"; -import { useProxy } from "valtio/utils"; -import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan"; -import { useState } from "react"; -import { useShallowEffect } from "@mantine/hooks"; import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useProxy } from "valtio/utils"; +import BackButton from "../../desa/layanan/_com/BackButto"; const manfaatProgram = [ { @@ -58,11 +57,12 @@ export default function Page() { const state = useProxy(programKesehatan); const router = useRouter(); const [search, setSearch] = useState(""); + const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const { data, page, totalPages, loading, load } = state.findMany; useShallowEffect(() => { - load(page, 3, search); - }, [page, search]); + load(page, 3, debouncedSearch); + }, [page, debouncedSearch]); if (loading || !data) { return ( @@ -87,7 +87,7 @@ export default function Page() { > Program Kesehatan Desa
- + Temukan berbagai program kesehatan untuk mendukung kualitas hidup masyarakat Darmasaba. @@ -125,14 +125,37 @@ export default function Page() { className="hover-scale" > - {v.name} +
+ + {v.name} (e.currentTarget.style.transform = 'scale(1.05)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')} + /> + + + +
+ @@ -154,13 +178,13 @@ export default function Page() { {v.createdAt ? new Date(v.createdAt).toLocaleDateString( - "id-ID", - { - day: "numeric", - month: "long", - year: "numeric", - } - ) + "id-ID", + { + day: "numeric", + month: "long", + year: "numeric", + } + ) : "Tanggal tidak tersedia"} @@ -169,7 +193,6 @@ export default function Page() { Admin Desa - -
@@ -224,7 +246,7 @@ export default function Page() { > Manfaat Program Kesehatan
- + Program kesehatan Desa Darmasaba berperan penting dalam meningkatkan kesejahteraan dan kualitas hidup warganya. @@ -254,7 +276,7 @@ export default function Page() { {v.title} - + {v.desc} diff --git a/src/app/darmasaba/(pages)/kesehatan/puskesmas/page.tsx b/src/app/darmasaba/(pages)/kesehatan/puskesmas/page.tsx index 2e324920..ed15017d 100644 --- a/src/app/darmasaba/(pages)/kesehatan/puskesmas/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/puskesmas/page.tsx @@ -43,7 +43,7 @@ function Page() { Daftar Puskesmas - + Temukan informasi lengkap mengenai layanan, kontak, dan lokasi Puskesmas Darmasaba @@ -93,20 +93,23 @@ function Page() { {v.name} Aktif - - - - {v.alamat} + + + + {v.alamat} - - + + + {v.kontak.kontakPuskesmas} - - + + + {v.kontak.email} + setSearch(e.currentTarget.value)} /> - - Desa Darmasaba menjaga dan mengembangkan lingkungan demi kesejahteraan warganya. Fokus utama meliputi penghijauan, pengelolaan sampah, dan perlindungan kawasan hijau. + + Desa Darmasaba menjaga dan mengembangkan lingkungan demi kesejahteraan warganya. + + + Fokus utama meliputi penghijauan, pengelolaan sampah, dan perlindungan kawasan hijau. @@ -104,7 +107,7 @@ function Page() { ± {item.jumlah} - + {item.name} diff --git a/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/component/edukasiCard.tsx b/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/component/edukasiCard.tsx new file mode 100644 index 00000000..92fe9f3a --- /dev/null +++ b/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/component/edukasiCard.tsx @@ -0,0 +1,63 @@ +// Create a new component: components/EdukasiCard.tsx +'use client'; + +import { Box, Paper, Stack, Text } from '@mantine/core'; +import { ReactNode } from 'react'; + +interface EdukasiCardProps { + icon: ReactNode; + title: string; + description: string; + color?: string; +} + +export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: EdukasiCardProps) { + return ( + + + + + {icon} + + + + + + + ); +} \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/page.tsx b/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/page.tsx index aa559a6d..14a4a925 100644 --- a/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/page.tsx @@ -1,91 +1,103 @@ -'use client' -import colors from '@/con/colors'; -import { Box, List, ListItem, Paper, SimpleGrid, Stack, Text, Tooltip } from '@mantine/core'; +'use client'; + +import { Box, Container, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react'; +import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; -const data = [ - { - id: 1, - title: 'Tujuan Edukasi Lingkungan', - icon: , - listDeskripsi: [ - 'Meningkatkan kesadaran masyarakat akan pentingnya lingkungan bersih dan sehat', - 'Mendorong partisipasi warga dalam pengelolaan sampah, penghijauan, dan konservasi', - 'Mengurangi dampak negatif kegiatan manusia terhadap lingkungan', - 'Membentuk generasi muda peduli isu-isu lingkungan', - ], - }, - { - id: 2, - title: 'Materi Edukasi yang Diberikan', - icon: , - listDeskripsi: [ - 'Pengelolaan sampah: pilah organik & anorganik', - 'Pencegahan pencemaran lingkungan (air, udara, tanah)', - 'Pemanfaatan lahan hijau dan penghijauan desa', - 'Daur ulang dan kreativitas dari sampah', - 'Bahaya pembakaran sampah sembarangan', - ], - }, - { - id: 3, - title: 'Contoh Kegiatan di Desa Darmasaba', - icon: , - listDeskripsi: [ - 'Pelatihan membuat kompos dari sampah rumah tangga', - 'Gerakan "Jumat Bersih" rutin', - 'Workshop pembuatan ecobrick', - 'Lomba kebersihan antar banjar', - 'Sosialisasi lingkungan di sekolah dan posyandu', - ], - }, -]; +import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan'; +import colors from '@/con/colors'; +import { EdukasiCard } from './component/edukasiCard'; -function Page() { +function LoadingSkeleton() { return ( - + + {[1, 2, 3].map((item) => ( + + ))} + + ); +} + +export default function EdukasiLingkunganPage() { + const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById); + const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById); + const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById); + + useShallowEffect(() => { + tujuan.load('edit'); + materi.load('edit'); + contoh.load('edit'); + }, []); + + const isLoading = tujuan.loading || !tujuan.data || + materi.loading || !materi.data || + contoh.loading || !contoh.data; + + if (isLoading) { + return ( + + + + + ); + } + + return ( + - - + + Edukasi Lingkungan - + Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam, meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama. - + - - - {data.map((item) => ( - - - - - - {item.icon} - - {item.title} - - - - - - {item.listDeskripsi.map((desc, idx) => ( - {desc} - ))} - - - - ))} + + + } + title={tujuan.data?.judul || ''} + description={tujuan.data?.deskripsi || ''} + color={colors['blue-button']} + /> + + } + title={materi.data?.judul || ''} + description={materi.data?.deskripsi || ''} + color={colors['blue-button']} + /> + + } + title={contoh.data?.judul || ''} + description={contoh.data?.deskripsi || ''} + color={colors['blue-button']} + /> - + ); -} - -export default Page; +} \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/[id]/page.tsx b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/[id]/page.tsx index 788730e3..fc930270 100644 --- a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/[id]/page.tsx @@ -1,80 +1,132 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; -import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; -import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; -import colors from '@/con/colors'; -import { Box, Center, Container, Image, Skeleton, Stack, Text } from '@mantine/core'; -import { useParams } from 'next/navigation'; -import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; +import { Box, Image, Paper, Skeleton, Stack, Text, Title, Group } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconCalendar, IconMapPin, IconUsers } from '@tabler/icons-react'; +import { useParams } from 'next/navigation'; +import colors from '@/con/colors'; +import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; +function DetailKegiatanDesaUser() { + const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa); + const params = useParams(); + useShallowEffect(() => { + kegiatanDesaState.findUnique.load(params?.id as string); + }, []); + const data = kegiatanDesaState.findUnique.data; -function Page() { - const params = useParams<{ id: string }>(); - const id = Array.isArray(params.id) ? params.id[0] : params.id; - const state = useProxy(gotongRoyongState.kegiatanDesa) - const [loading, setLoading] = useState(true) - - useEffect(() => { - const loadData = async () => { - if (!id) return; - try { - setLoading(true); - await state.findUnique.load(id); - } catch (error) { - console.error('Error loading data:', error); - } finally { - setLoading(false); - } - } - loadData() - }, [id]) - - if (loading) { + if (!data) { return ( -
- -
+ + + + + + ); } - if (!state.findUnique.data) { - return ( -
- Data tidak ditemukan -
- ); - } - - return ( - - - - - - {state.findUnique.data?.judul} - - - Informasi Kegiatan Gotong Royong - - - - - - - + + {/* Header Gambar */} + + {/* Konten */} + + + {data.image?.link && ( + {data.judul + )} + {/* Judul */} + + {data.judul || 'Kegiatan Desa'} + + + {/* Meta Info */} + + + + + {data.tanggal ? new Date(data.tanggal).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' }) : '-'} + + + + {data.lokasi && ( + + + {data.lokasi} + + )} + + {data.partisipan && ( + + + {data.partisipan} + + )} + + + {/* Deskripsi Singkat */} + {data.deskripsiSingkat && ( + + + Ringkasan + + + + )} + + {/* Deskripsi Lengkap */} + {data.deskripsiLengkap && ( + + + Detail Kegiatan + + + + )} + + {/* Kategori */} + {data.kategoriKegiatan?.nama && ( + + + Kategori + + + {data.kategoriKegiatan.nama} + + + )} - - + + ); } -export default Page; +export default DetailKegiatanDesaUser; diff --git a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/content.tsx b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/content.tsx index 1c38fa7d..58ab46f1 100644 --- a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/content.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/content.tsx @@ -1,5 +1,9 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' +import { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Transition } from '@mantine/core'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import { Badge, @@ -23,12 +27,11 @@ import { } from '@mantine/core'; import { IconArrowRight, IconCalendar } from '@tabler/icons-react'; import { useTransitionRouter } from 'next-view-transitions'; -import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; export default function Content({ kategori }: { kategori: string }) { const router = useTransitionRouter(); const [page, setPage] = useState(1); + const [animateKey, setAnimateKey] = useState(0); const state = useProxy(gotongRoyongState.kegiatanDesa); const featuredState = useProxy(gotongRoyongState.kegiatanDesa.findFirst); @@ -37,119 +40,178 @@ export default function Content({ kategori }: { kategori: string }) { const paginatedNews = state.findMany.data || []; const totalPages = state.findMany.totalPages || 1; - // Load data + // Load data awal useEffect(() => { gotongRoyongState.kegiatanDesa.findFirst.load(kategori); }, [kategori]); + // Load daftar berita useEffect(() => { state.findMany.load(page, 3, '', kategori); + setAnimateKey((prev) => prev + 1); // trigger animasi halus saat page berubah }, [page, kategori]); + // Tampilan kosong + if (!featuredState.loading && !featured) { + return ( +
+ + Belum Ada Data Gotong Royong + Tidak ada data gotong royong yang tersedia saat ini. + +
+ ); + } + return ( {/* === Gotong Royong Utama === */} - {featuredState.loading ? ( -
- ) : featured ? ( - - Gotong Royong Utama - - - - {featured.judul - - - -
- - {featured.kategoriKegiatan?.nama || kategori} - - {featured.judul} - {featured.deskripsiLengkap} -
- - - - - {new Date(featured.createdAt).toLocaleDateString('id-ID', { - day: 'numeric', - month: 'long', - year: 'numeric', - })} - - - - -
-
-
-
-
- ) : null} + + {(styles) => ( +
+ {featured ? ( + + Gotong Royong Utama + + + + {featured.judul + + + +
+ + {featured.kategoriKegiatan?.nama || kategori} + + {featured.judul} + +
+ + + + + {new Date(featured.createdAt).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + + + + +
+
+
+
+
+ ) : ( + + )} +
+ )} +
- {/* === Daftar Gotong Royong === */} + {/* === Daftar Gotong Royong (Pagination + Fade-in Halus) === */} Daftar Gotong Royong - {state.findMany.loading ? ( - - {Array(3).fill(0).map((_, i) => ( - - ))} - - ) : paginatedNews.length === 0 ? ( - Belum ada gotong royong di kategori "{kategori}". - ) : ( - - {paginatedNews.map((item) => ( - router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${item.id}`)} - style={{ cursor: 'pointer' }} - > - - {item.judul} - - - {item.kategoriKegiatan?.nama || kategori} - - {item.judul} - - - - {new Date(item.createdAt).toLocaleDateString('id-ID', { - day: 'numeric', - month: 'short', - year: 'numeric', - })} - - Baca Selengkapnya - - - ))} - - )} + + + {state.findMany.loading ? ( + + {Array(3) + .fill(0) + .map((_, i) => ( + + ))} + + ) : paginatedNews.length === 0 ? ( +
+ + Tidak Ada Data + Belum ada data gotong royong yang tersedia. + +
+ ) : ( + + {paginatedNews.map((item) => ( + router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${item.id}`)} + style={{ cursor: 'pointer' }} + > + + {item.judul} + + + {item.kategoriKegiatan?.nama || kategori} + + + {item.judul} + + + + + {new Date(item.createdAt).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + })} + + Baca Selengkapnya + + + ))} + + )} +
+
{/* Pagination */}
@@ -166,4 +228,4 @@ export default function Content({ kategori }: { kategori: string }) { ); -} \ No newline at end of file +} diff --git a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/page.tsx b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/page.tsx index 3c40cc0c..fe20be98 100644 --- a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/page.tsx @@ -1,4 +1,3 @@ -// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx import { Suspense } from "react"; import Content from "./content"; diff --git a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/_lib/layoutTabs.tsx b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/_lib/layoutTabs.tsx index 21371f49..1ef77765 100644 --- a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/_lib/layoutTabs.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/_lib/layoutTabs.tsx @@ -1,113 +1,73 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + 'use client' +import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import colors from '@/con/colors'; -import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core'; +import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core'; import { IconSearch } from '@tabler/icons-react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import React, { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; import BackButton from '../../../desa/layanan/_com/BackButto'; - -type HeaderSearchProps = { - placeholder?: string; - searchIcon?: React.ReactNode; - value?: string; - onChange?: (event: React.ChangeEvent) => void; - children?: React.ReactNode; -}; - -function LayoutTabsGotongRoyong({ - children, - placeholder = "pencarian", - searchIcon = -}: HeaderSearchProps) { +function LayoutTabsGotongRoyong({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); - - // Get active tab from URL path + + const kategoriState = useProxy(gotongRoyongState.kategoriKegiatan); + + // tab aktif dari url const activeTab = pathname.split('/').pop() || 'semua'; - - // Get initial search value from URL - const initialSearch = searchParams.get('search') || ''; - const [searchValue, setSearchValue] = useState(initialSearch); - const [searchTimeout, setSearchTimeout] = useState(null); - - // Update active tab state when pathname changes const [activeTabState, setActiveTabState] = useState(activeTab); + + useEffect(() => { + kategoriState.findMany.load(); // ambil kategori dari DB + }, []); + useEffect(() => { setActiveTabState(activeTab); }, [activeTab]); - // Clean up timeouts on unmount - useEffect(() => { - return () => { - if (searchTimeout !== null) { - clearTimeout(searchTimeout); - } - }; - }, [searchTimeout]); + // search + const initialSearch = searchParams.get('search') || ''; + const [searchValue, setSearchValue] = useState(initialSearch); + const [searchTimeout, setSearchTimeout] = useState(null); - // Handle search input change with debounce const handleSearchChange = (event: React.ChangeEvent) => { const value = event.target.value; setSearchValue(value); - - // Clear previous timeout - if (searchTimeout !== null) { - clearTimeout(searchTimeout); - } - - // Set new timeout + + if (searchTimeout !== null) clearTimeout(searchTimeout); + const newTimeout = window.setTimeout(() => { const params = new URLSearchParams(searchParams.toString()); - - if (value) { - params.set('search', value); - } else { - params.delete('search'); - } - - // Only update URL if the search value has actually changed - if (params.toString() !== searchParams.toString()) { - router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`); - } - }, 500); // 500ms debounce delay - + if (value) params.set('search', value); + else params.delete('search'); + + router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}${params.toString() ? `?${params.toString()}` : ''}`); + }, 500); + setSearchTimeout(newTimeout); }; + + // --- tabs dinamis --- const tabs = [ - { - label: "Semua", - value: "semua", - href: "/darmasaba/lingkungan/gotong-royong/semua" - }, - { - label: "Kebersihan", - value: "kebersihan", - href: "/darmasaba/lingkungan/gotong-royong/kebersihan" - }, - { - label: "Infrastruktur", - value: "infrastruktur", - href: "/darmasaba/lingkungan/gotong-royong/infrastruktur" - }, - { - label: "Sosial", - value: "sosial", - href: "/darmasaba/lingkungan/gotong-royong/sosial" - }, - { - label: "Lingkungan", - value: "lingkungan", - href: "/darmasaba/lingkungan/gotong-royong/lingkungan" - } + { label: "Semua", value: "semua", href: "/darmasaba/lingkungan/gotong-royong/semua" }, + ...(kategoriState.findMany.data || []).map((kat: any) => ({ + label: kat.nama, + value: kat.nama.toLowerCase(), + href: `/darmasaba/lingkungan/gotong-royong/${kat.nama.toLowerCase()}` + })) ]; + const handleTabChange = (value: string | null) => { if (!value) return; const tab = tabs.find(t => t.value === value); if (tab) { const params = new URLSearchParams(searchParams.toString()); - router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`); + router.push(`${tab.href}${params.toString() ? `?${params.toString()}` : ''}`); } }; @@ -117,17 +77,29 @@ function LayoutTabsGotongRoyong({ - - - - Gotong Royong Desa Darmasaba - - - Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba - - - + + + + + Portal Gotong royong Darmasaba + + Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba + + + } + w="100%" + value={searchValue} + onChange={handleSearchChange} + /> + + + + + {/* TABS */} - - - - {tabs.map((tab, index) => ( - router.push(tab.href)} - > - {tab.label} - - ))} - - - - - - + + + {tabs.map((tab, index) => ( + router.push(tab.href)} + style={{ + flex: '0 0 auto', + minWidth: 100, + textAlign: 'center' + }} + > + {tab.label} + + ))} + + {children} @@ -168,4 +133,4 @@ function LayoutTabsGotongRoyong({ ); } -export default LayoutTabsGotongRoyong; \ No newline at end of file +export default LayoutTabsGotongRoyong; diff --git a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/semua/page.tsx b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/semua/page.tsx index 8f6748cf..785e5b92 100644 --- a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/semua/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/semua/page.tsx @@ -1,175 +1,282 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; -import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core'; +import { + Badge, + Box, + Button, + Card, + Center, + Container, + Divider, + Flex, + Grid, + GridCol, + Group, + Image, + Pagination, + Paper, + SimpleGrid, + Skeleton, + Stack, + Text, + Title, + Transition, +} from '@mantine/core'; import { IconArrowRight, IconCalendar } from '@tabler/icons-react'; -import { useTransitionRouter } from 'next-view-transitions'; -import { useSearchParams } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect } from 'react'; import { useProxy } from 'valtio/utils'; -function Page() { +export default function Page() { const searchParams = useSearchParams(); - const router = useTransitionRouter(); + const router = useRouter(); - // Parameter URL const search = searchParams.get('search') || ''; - const currentPage = parseInt(searchParams.get('page') || '1'); - const [page, setPage] = useState(currentPage); + const page = parseInt(searchParams.get('page') || '1'); - // Gunakan proxy untuk state const state = useProxy(gotongRoyongState.kegiatanDesa); - const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst); // ✅ Berita utama + const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst); const loadingGrid = state.findMany.loading; const loadingFeatured = featured.loading; - // Load berita utama (hanya sekali) + // Load featured data once on component mount useEffect(() => { - if (!featured.data && !loadingFeatured) { - gotongRoyongState.kegiatanDesa.findFirst.load(); + let mounted = true; + + const loadFeatured = async () => { + try { + if (!featured.data && !loadingFeatured) { + await gotongRoyongState.kegiatanDesa.findFirst.load(); + } + } catch (error) { + console.error('Error loading featured data:', error); + } + }; + + if (mounted) { + loadFeatured(); } - }, [featured.data, loadingFeatured]); - // Load berita terbaru (untuk grid) saat page/search berubah + return () => { + mounted = false; + }; + }, []); // Empty dependency array to run only once on mount + useEffect(() => { - const limit = 3; // Sesuaikan dengan tampilan grid - state.findMany.load(page, limit, search); + let mounted = true; + + const loadData = async () => { + try { + const limit = 3; + await state.findMany.load(page, limit, search); + } catch (error) { + console.error('Error loading data:', error); + } + }; + + if (mounted) { + loadData(); + } + + return () => { + mounted = false; + }; }, [page, search]); - // Update URL saat page berubah - useEffect(() => { - const url = new URLSearchParams(); + const handlePageChange = (newPage: number) => { + const url = new URLSearchParams(searchParams.toString()); if (search) url.set('search', search); - if (page > 1) url.set('page', page.toString()); - router.replace(`?${url.toString()}`); - }, [page, search]); + if (newPage > 1) url.set('page', newPage.toString()); + else url.delete('page'); + + // Use push instead of replace to keep browser history + router.push(`?${url.toString()}`, { scroll: false }); + }; const featuredData = featured.data; const paginatedNews = state.findMany.data || []; const totalPages = state.findMany.totalPages || 1; - return ( - - - {/* === Gotong royong Utama (Tetap) === */} - {loadingFeatured ? ( -
- ) : featuredData ? ( - - Gotong royong Utama - - - - {featuredData.judul - - - -
- - {featuredData.kategoriKegiatan?.nama || 'Gotong royong'} - - {featuredData.judul} - - {featuredData.deskripsiSingkat} - -
- - - - - {new Date(featuredData.createdAt).toLocaleDateString('id-ID', { - day: 'numeric', - month: 'long', - year: 'numeric' - })} - - - - -
-
-
-
-
- ) : null} + // Animasi transisi halus tapi tetap instant load + const MotionBox = motion(Box as any); - {/* === Gotong royong Terbaru (Berubah Saat Pagination) === */} + // fallback kosong + if (!loadingGrid && !loadingFeatured && paginatedNews.length === 0) { + return ( + + Belum Ada Data Gotong Royong + Tidak ada data gotong royong yang tersedia saat ini. + + ); + } + + return ( + + + {/* === Gotong Royong Utama === */} + + {(styles) => + featuredData ? ( + + Gotong royong Utama + + + + {featuredData.judul + + + +
+ + {featuredData.kategoriKegiatan?.nama || 'Gotong royong'} + + {featuredData.judul} + +
+ + + + + {new Date(featuredData.createdAt).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + + + + +
+
+
+
+
+ ) : ( + + ) + } +
+ + {/* === Gotong royong Terbaru === */} Gotong royong Terbaru - {loadingGrid ? ( - - {Array(3).fill(0).map((_, i) => ( - - ))} - - ) : paginatedNews.length === 0 ? ( - Tidak ada gotong royong ditemukan. - ) : ( - - {paginatedNews.map((item) => ( - - - {item.judul} - + + {(styles) => + loadingGrid ? ( + + {Array(3) + .fill(0) + .map((_, i) => ( + + ))} + + ) : paginatedNews.length === 0 ? ( + + Tidak ada gotong royong ditemukan. + + ) : ( + + + {paginatedNews.map((item) => ( + + + {item.judul} + - - {item.kategoriKegiatan?.nama || 'Gotong royong'} - + + {item.kategoriKegiatan?.nama || 'Gotong royong'} + - {item.judul} + + {item.judul} + - {item.deskripsiSingkat} + - - - {new Date(item.createdAt).toLocaleDateString('id-ID', { - day: 'numeric', - month: 'short', - year: 'numeric' - })} - - - - - - ))} - - )} + + + {new Date(item.createdAt).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + })} + - {/* Pagination hanya untuk berita terbaru */} + + + + ))} + + + ) + } + + + {/* Pagination */}
- + ); } - -export default Page; - diff --git a/src/app/darmasaba/(pages)/lingkungan/konservasi-adat-bali/page.tsx b/src/app/darmasaba/(pages)/lingkungan/konservasi-adat-bali/page.tsx index 73360da3..76a39cf6 100644 --- a/src/app/darmasaba/(pages)/lingkungan/konservasi-adat-bali/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/konservasi-adat-bali/page.tsx @@ -1,47 +1,30 @@ +'use client' +import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali'; import colors from '@/con/colors'; -import { Box, Center, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; +import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; -const data = [ - { - id: 1, - title: 'Filosofi Tri Hita Karana', - listDeskripsi: ( - - Parahyangan: Hubungan manusia dengan Tuhan yang dijaga penuh kesadaran spiritual - Pawongan: Harmoni dan kerja sama antar manusia dalam masyarakat - Palemahan: Pelestarian lingkungan dan hubungan manusia dengan alam - - ), - }, - { - id: 2, - title: 'Bentuk Konservasi Berdasarkan Adat', - listDeskripsi: ( - - Pelestarian Hutan Adat seperti Alas Pala Sangeh dan Wana Kerthi - Subak: Sistem irigasi tradisional yang menekankan kebersamaan dan keberlanjutan - Hari Raya Tumpek Uduh: Perayaan untuk menghormati pohon dan tumbuhan - Perarem & Awig-Awig: Aturan adat untuk menjaga lingkungan dari kerusakan - Ritual penyucian alam seperti Melasti dan Piodalan Segara - - ), - }, - { - id: 3, - title: 'Nilai Konservasi Adat', - listDeskripsi: ( - - Menjaga keseimbangan ekosistem dan lingkungan hidup - Melestarikan spiritualitas lokal dan kesucian alam - Meningkatkan kesadaran kolektif untuk hidup selaras dengan alam - Menjamin keberlanjutan sumber daya alam untuk generasi mendatang - - ), - }, -]; - function Page() { + const filosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita.findById) + const nilai = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat.findById) + const bentuk = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat.findById) + + useShallowEffect(() => { + filosofi.load('edit') + nilai.load('edit') + bentuk.load('edit') + }, []) + + if (filosofi.loading || !filosofi.data || nilai.loading || !nilai.data || bentuk.loading || !bentuk.data) { + return ( + + + + ); + } + return ( @@ -56,24 +39,96 @@ function Page() { - - {data.map((item) => ( + + {/* Filsosofi */} + - +
- {item.title} - + {filosofi.data?.judul} +
- {item.listDeskripsi} +
- ))} + + {/* Nilai */} + + + +
+ + {nilai.data?.judul} + +
+
+ + + + {/* Bentuk */} + + + +
+ + {bentuk.data?.judul} + +
+
+ + + diff --git a/src/app/darmasaba/(pages)/lingkungan/pengelolaan-sampah-bank-sampah/page.tsx b/src/app/darmasaba/(pages)/lingkungan/pengelolaan-sampah-bank-sampah/page.tsx index ee92f96d..d50df75c 100644 --- a/src/app/darmasaba/(pages)/lingkungan/pengelolaan-sampah-bank-sampah/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/pengelolaan-sampah-bank-sampah/page.tsx @@ -1,10 +1,10 @@ 'use client' import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import colors from '@/con/colors'; -import { Box, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react'; -import React from 'react'; +import { Box, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconRoute, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react'; +import React, { useState } from 'react'; import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; import dynamic from 'next/dynamic'; @@ -20,20 +20,26 @@ function Page() { const state = useProxy(pengelolaanSampahState.pengelolaanSampah) const state2 = useProxy(pengelolaanSampahState.keteranganSampah) + const [search, setSearch] = useState('') + const [debouncedSearch] = useDebouncedValue(search, 500); + const { data, - load + load, + } = state.findMany const { data: data2, - load: load2 + load: load2, + page, + totalPages, } = state2.findMany useShallowEffect(() => { load() - load2() - }, []) + load2(page, 3, debouncedSearch) + }, [page, debouncedSearch]) const iconMap: Record = { ekowisata: IconLeaf, @@ -85,7 +91,7 @@ function Page() { {iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null} - {v.name} + {v.name} @@ -104,8 +110,10 @@ function Page() { px={{ base: 70, md: 150 }} leftSection={} placeholder='Cari Bank Sampah Terdekat' + value={search} + onChange={(e) => setSearch(e.target.value)} /> - + {/* Left side - List of bank locations */} @@ -114,26 +122,42 @@ function Page() { {data2?.map((v, k) => ( - {v.namaTempatMaps} - {v.alamat} - {v.lat && v.lng ? ( - - 📌 Buka di Google Maps - - ) : ( - Koordinat belum tersedia - )} + + + {v.namaTempatMaps} + {v.alamat} + + + + Rute + + + {v.lat && v.lng ? ( + + 📌 Lihat Peta Lebih Besar + + ) : ( + Koordinat belum tersedia + )} ))} +
+ load(newPage)} // ini penting! + total={totalPages} + my="md" + /> +
- + {/* Right side - Single map showing all locations */} diff --git a/src/app/darmasaba/(pages)/lingkungan/program-penghijauan/[id]/page.tsx b/src/app/darmasaba/(pages)/lingkungan/program-penghijauan/[id]/page.tsx index 1922f7d1..1e33d357 100644 --- a/src/app/darmasaba/(pages)/lingkungan/program-penghijauan/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/program-penghijauan/[id]/page.tsx @@ -100,6 +100,7 @@ function Page() { lh={1.7} ta="justify" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> ) : ( diff --git a/src/app/darmasaba/(pages)/lingkungan/program-penghijauan/page.tsx b/src/app/darmasaba/(pages)/lingkungan/program-penghijauan/page.tsx index 025a08fc..e10c2dfc 100644 --- a/src/app/darmasaba/(pages)/lingkungan/program-penghijauan/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/program-penghijauan/page.tsx @@ -66,8 +66,11 @@ function Page() { /> - - Mari berpartisipasi menanam dan merawat pohon untuk menciptakan lingkungan hijau, sehat, dan seimbang bagi seluruh warga desa. + + Mari berpartisipasi menanam dan merawat pohon untuk menciptakan lingkungan hijau, + + + sehat, dan seimbang bagi seluruh warga desa. diff --git a/src/app/darmasaba/(pages)/musik/musik-desa/page.tsx b/src/app/darmasaba/(pages)/musik/musik-desa/page.tsx new file mode 100644 index 00000000..7c8ee6e8 --- /dev/null +++ b/src/app/darmasaba/(pages)/musik/musik-desa/page.tsx @@ -0,0 +1,248 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client' +import { ActionIcon, Avatar, Badge, Box, Card, Flex, Grid, Group, Paper, Slider, Stack, Text, TextInput } from '@mantine/core'; +import { IconArrowsShuffle, IconPlayerPauseFilled, IconPlayerPlayFilled, IconPlayerSkipBackFilled, IconPlayerSkipForwardFilled, IconRepeat, IconRepeatOff, IconSearch, IconVolume, IconVolumeOff, IconX } from '@tabler/icons-react'; +import { useEffect, useState } from 'react'; +import BackButton from '../../desa/layanan/_com/BackButto'; + +const MusicPlayer = () => { + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(245); + const [volume, setVolume] = useState(70); + const [isMuted, setIsMuted] = useState(false); + const [isRepeat, setIsRepeat] = useState(false); + const [isShuffle, setIsShuffle] = useState(false); + + const songs = [ + { id: 1, title: 'Midnight Dreams', artist: 'The Wanderers', duration: '4:05', cover: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop' }, + { id: 2, title: 'Summer Breeze', artist: 'Coastal Vibes', duration: '3:42', cover: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=400&h=400&fit=crop' }, + { id: 3, title: 'City Lights', artist: 'Urban Echo', duration: '4:18', cover: 'https://images.unsplash.com/photo-1514320291840-2e0a9bf2a9ae?w=400&h=400&fit=crop' }, + { id: 4, title: 'Ocean Waves', artist: 'Serenity Sound', duration: '5:20', cover: 'https://images.unsplash.com/photo-1459749411175-04bf5292ceea?w=400&h=400&fit=crop' }, + { id: 5, title: 'Neon Nights', artist: 'Electric Dreams', duration: '3:55', cover: 'https://images.unsplash.com/photo-1487180144351-b8472da7d491?w=400&h=400&fit=crop' }, + { id: 6, title: 'Mountain High', artist: 'Peak Performers', duration: '4:32', cover: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=400&h=400&fit=crop' } + ]; + + const [currentSong, setCurrentSong] = useState(songs[0]); + + useEffect(() => { + let interval: any; + if (isPlaying) { + interval = setInterval(() => { + setCurrentTime(prev => { + if (prev >= duration) { + setIsPlaying(false); + return 0; + } + return prev + 1; + }); + }, 1000); + } + return () => clearInterval(interval); + }, [isPlaying, duration]); + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + const playSong = (song: any) => { + setCurrentSong(song); + setCurrentTime(0); + setIsPlaying(true); + const durationInSeconds = parseInt(song.duration.split(':')[0]) * 60 + parseInt(song.duration.split(':')[1]); + setDuration(durationInSeconds); + }; + + const toggleMute = () => { + setIsMuted(!isMuted); + }; + + return ( + + + + + +
+ Selamat Datang Kembali + Temukan musik favorit Anda hari ini +
+ + } + radius="xl" + w={280} + styles={{ input: { backgroundColor: '#fff' } }} + /> + +
+ +
+ Sedang Diputar + + + + +
+ {currentSong.title} + {currentSong.artist} +
+ + {formatTime(currentTime)} + + {formatTime(duration)} + +
+
+
+
+ +
+ Daftar Putar + + {songs.map(song => ( + + playSong(song)} + > + + + + {song.title} + {song.artist} + {song.duration} + + {currentSong.id === song.id && isPlaying && ( + Memutar + )} + + + + ))} + +
+
+ +
+ +
+ + + + + +
+ {currentSong.title} + {currentSong.artist} +
+
+ + + + setIsShuffle(!isShuffle)} + radius="xl" + > + {isShuffle ? : } + + + + + setIsPlaying(!isPlaying)} + > + {isPlaying ? : } + + + + + setIsRepeat(!isRepeat)} + radius="xl" + > + {isRepeat ? : } + + + + {formatTime(currentTime)} + + {formatTime(duration)} + + + + + + {isMuted || volume === 0 ? : } + + { + setVolume(val); + if (val > 0) setIsMuted(false); + }} + color="#0B4F78" + size="xs" + w={100} + /> + {isMuted ? 0 : volume}% + +
+
+
+ ); +}; + +export default MusicPlayer; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx index dca68008..42a22ec3 100644 --- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx @@ -1,9 +1,10 @@ 'use client' import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa'; import colors from '@/con/colors'; -import { Box, Button, Center, Group, Image, Modal, Paper, Select, SimpleGrid, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core'; -import { useDisclosure } from '@mantine/hooks'; +import { Box, Button, Center, Divider, Group, Image, Modal, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Stepper, Text, TextInput, Title } from '@mantine/core'; +import { useDisclosure, useShallowEffect } from '@mantine/hooks'; import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react'; +import { useTransitionRouter } from 'next-view-transitions'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; @@ -14,42 +15,63 @@ const dataBeasiswa = [ { id: 3, nama: 'Dana Tersalurkan', jumlah: '1.5M', icon: IconCoin }, ]; -const dataProgram = [ - { id: 1, judul: "Pelatihan SoftSkill", deskripsi: "Pengembangan diri untuk mempersiapkan karir masa depan." }, - { id: 2, judul: "Peningkatan Akses Pendidikan", deskripsi: "Memberi kesempatan bagi masyarakat kurang mampu untuk tetap sekolah." }, - { id: 3, judul: "Pendampingan Intensif", deskripsi: "Bimbingan dari mentor berpengalaman untuk mendukung akademik." }, -]; - function Page() { const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar) + const ungggulanDesa = useProxy(beasiswaDesaState.keunggulanProgram) const [opened, { open, close }] = useDisclosure(false); + const router = useTransitionRouter() const resetForm = () => { beasiswaDesa.create.form = { namaLengkap: "", - nik: "", + nis: "", + kelas: "", + jenisKelamin: "", + alamatDomisili: "", tempatLahir: "", tanggalLahir: "", - jenisKelamin: "", - kewarganegaraan: "", - agama: "", - alamatKTP: "", - alamatDomisili: "", + namaOrtu: "", + nik: "", + pekerjaanOrtu: "", + penghasilan: "", noHp: "", - email: "", - statusPernikahan: "", - ukuranBaju: "", }; }; + const { data, page, totalPages, loading, load } = ungggulanDesa.findMany; + + useShallowEffect(() => { + load(page, 3, ""); + }, [page]) + const handleSubmit = async () => { await beasiswaDesa.create.create(); resetForm(); close(); }; + const timeline = [ + { label: "1 Maret 2025", desc: "Pembukaan Pendaftaran", date: new Date("2025-03-01") }, + { label: "15 Maret 2025", desc: "Seleksi Administrasi", date: new Date("2025-03-15") }, + { label: "1 April 2025", desc: "Tes Potensi Akademik", date: new Date("2025-04-01") }, + { label: "15 April 2025", desc: "Wawancara", date: new Date("2025-04-15") }, + { label: "1 Mei 2025", desc: "Pengumuman Hasil", date: new Date("2025-05-01") }, + ]; + const [active, setActive] = useState(1); - const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current)); - const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); + useShallowEffect(() => { + const today = new Date(); + // cari berapa banyak tanggal yang sudah lewat + const doneSteps = timeline.filter(item => today >= item.date).length; + setActive(doneSteps); // active step diset sesuai tanggal + }, []); + + if (loading || !data) { + return ( + + + + ); + } return ( @@ -63,20 +85,20 @@ function Page() { Wujudkan Mimpi Pendidikanmu di Desa Darmasaba - + Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba. -
- Beasiswa Desa + Beasiswa Desa
@@ -101,31 +123,49 @@ function Page() { Keunggulan Program - {dataProgram.map((v, k) => ( + {data.map((v, k) => ( {v.judul} - {v.deskripsi} + ))} +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+ Timeline Pendaftaran
- - - - - - + + {timeline.map((item, index) => ( + + ))}
- - - - - - { beasiswaDesa.create.form.namaLengkap = val.target.value }} /> - { beasiswaDesa.create.form.nik = val.target.value }} /> - { beasiswaDesa.create.form.tempatLahir = val.target.value }} /> - { beasiswaDesa.create.form.tanggalLahir = val.target.value }} /> - { if (val) beasiswaDesa.create.form.agama = val }} /> - { beasiswaDesa.create.form.alamatKTP = val.target.value }} /> - { beasiswaDesa.create.form.alamatDomisili = val.target.value }} /> - { beasiswaDesa.create.form.noHp = val.target.value }} /> - { beasiswaDesa.create.form.email = val.target.value }} /> - { if (val) beasiswaDesa.create.form.ukuranBaju = val }} /> + { beasiswaDesa.create.form.namaLengkap = val.target.value }} /> + { beasiswaDesa.create.form.nis = val.target.value }} /> + { beasiswaDesa.create.form.kelas = val.target.value }} /> + { if (val) beasiswaDesa.create.form.jenisKelamin = val }} /> + { beasiswaDesa.create.form.alamatDomisili = val.target.value }} /> + { beasiswaDesa.create.form.tempatLahir = val.target.value }} /> + { beasiswaDesa.create.form.tanggalLahir = val.target.value }} /> + + + + { beasiswaDesa.create.form.namaOrtu = val.target.value }} /> + { beasiswaDesa.create.form.nik = val.target.value }} /> + { beasiswaDesa.create.form.pekerjaanOrtu = val.target.value }} /> + { beasiswaDesa.create.form.penghasilan = val.target.value }} /> + { beasiswaDesa.create.form.noHp = val.target.value }} /> + + + + + + + + + ); +} diff --git a/src/app/darmasaba/(pages)/pendidikan/bimbingan-belajar-desa/page.tsx b/src/app/darmasaba/(pages)/pendidikan/bimbingan-belajar-desa/page.tsx index 85fbf4bf..2dbe5769 100644 --- a/src/app/darmasaba/(pages)/pendidikan/bimbingan-belajar-desa/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/bimbingan-belajar-desa/page.tsx @@ -1,10 +1,10 @@ 'use client' import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa'; import colors from '@/con/colors'; -import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge } from '@mantine/core'; +import { Box, Divider, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; +import { IconBook2, IconCalendarTime, IconMapPin } from '@tabler/icons-react'; import { useProxy } from 'valtio/utils'; -import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react'; import BackButton from '../../desa/layanan/_com/BackButto'; function Page() { @@ -49,47 +49,47 @@ function Page() { - - - Tujuan Program - + - - + + {stateTujuanProgram.findById.data?.judul} + + + - - - Lokasi & Jadwal - + - - + + {stateLokasiDanJadwal.findById.data?.judul} + + + - - - Fasilitas - + - - + + {stateFasilitas.findById.data?.judul} + + + diff --git a/src/app/darmasaba/(pages)/pendidikan/data-pendidikan/page.tsx b/src/app/darmasaba/(pages)/pendidikan/data-pendidikan/page.tsx index 0772bd13..9ac1be46 100644 --- a/src/app/darmasaba/(pages)/pendidikan/data-pendidikan/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/data-pendidikan/page.tsx @@ -57,7 +57,7 @@ function Page() { Statistik Data Pendidikan - + Visualisasi jumlah pendidikan berdasarkan kategori yang tersedia @@ -92,7 +92,7 @@ function Page() { cursor={{ fill: 'var(--mantine-color-gray-1)' }} /> - + diff --git a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/content.tsx b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/content.tsx index 2bf30eea..40fe3885 100644 --- a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/content.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/content.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; +import colors from '@/con/colors'; import { Box, Button, Center, Container, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip, ActionIcon } from '@mantine/core'; import { IconChalkboard, IconMicroscope, IconProps, IconRefresh, IconSchool, IconInfoCircle } from '@tabler/icons-react'; import { motion } from 'framer-motion'; @@ -23,19 +24,19 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan: const router = useTransitionRouter(); const [stats, setStats] = useState([]); const [isLoading, setIsLoading] = useState(true); - + // Decode the URL parameter const decodedJenjangPendidikan = decodeURIComponent(jenjangPendidikan); - const jenjangFilter = decodedJenjangPendidikan.toLowerCase() === 'semua' - ? undefined + const jenjangFilter = decodedJenjangPendidikan.toLowerCase() === 'semua' + ? undefined : decodedJenjangPendidikan; const loadData = useCallback(async () => { if (!decodedJenjangPendidikan) return; - + try { setIsLoading(true); - + // Load all data in parallel with the jenjang filter await Promise.all([ infoSekolahPaud.lembagaPendidikan.findMany.load(1, 100, '', jenjangFilter), @@ -50,7 +51,7 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan: setStats([ { - + icon: IconChalkboard, jumlah: totalLembaga, nama: 'Lembaga Pendidikan', @@ -119,11 +120,15 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan: @@ -143,7 +148,7 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan: aria-label="Tidak ada hasil" >
- + Tidak ditemukan @@ -173,81 +178,81 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan: style={{ width: '100%' }} > - - -
- - {React.createElement(v.icon, { - color: '#2563eb', - size: 34, - stroke: 1.6, - })} - -
+ + +
+ + {React.createElement(v.icon, { + color: colors['blue-button'], + size: 34, + stroke: 1.6, + })} + +
- - - - {v.jumlah.toLocaleString()} - - - - {v.nama} + + + + {v.jumlah.toLocaleString()} - - - - - - - - -
+ + + {v.nama} + + + + + + + +
+ + - - - -
+ + + +
)) diff --git a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/lembaga/page.tsx b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/lembaga/page.tsx index 801854da..1a66e860 100644 --- a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/lembaga/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/lembaga/page.tsx @@ -55,7 +55,7 @@ function Page({ params }: PageProps) { - Daftar Lembaga Pendidikan + Daftar Lembaga Pendidikan - Daftar Pengajar + Daftar Pengajar Nama Pengajar Nama Lembaga - Jenjang Pendidikan + Mengajar Di Jenjang Pendidikan @@ -95,7 +95,7 @@ function Page({ params }: PageProps) { {item.lembaga.jenjangPendidikan?.nama || '-'} ))} - + )} diff --git a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/siswa/page.tsx b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/siswa/page.tsx index 45317ac3..2107f6ad 100644 --- a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/siswa/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/[jenjangPendidikan]/siswa/page.tsx @@ -55,7 +55,7 @@ function Page({ params }: PageProps) { - Daftar Siswa + Daftar Siswa { +// // arahkan langsung ke route jenjang pendidikan +// if (k.toLowerCase() === 'semua') { +// setJenjangPendidikanAktif(k); +// router.push(`/darmasaba/pendidikan/info-sekolah/semua`); +// } else { +// setJenjangPendidikanAktif(k); +// router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`); +// } +// }; + + +// return ( +// +// +// +// {/* Back Button */} +// window.history.back()} variant="light" radius="md" size="lg"> +// +// Kembali +// + +// {/* Search & Filter */} +// +// +// {title} + +// +// Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu. +// +// +// {jenjangPendidikanList.map((k) => { +// const aktif = k === jenjangPendidikanAktif; +// return ( +// +// ); +// })} +// +// +// + +// {/* Slot konten */} +// {children} +// +// +// +// ); +// } 'use client' import colors from '@/con/colors'; + // pastikan path benar +import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import { - ActionIcon, Box, Button, Container, Group, + Loader, Paper, Stack, - Text, - VisuallyHidden + Text } from '@mantine/core'; -import { IconArrowLeft } from '@tabler/icons-react'; import { useRouter, useSearchParams } from 'next/navigation'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useSnapshot } from 'valtio'; +import BackButton from '../../../desa/layanan/_com/BackButto'; type LayoutSekolahProps = { title?: string; - jenjangPendidikanList?: string[]; children: React.ReactNode; }; export default function LayoutSekolah({ title = 'Cari Informasi Sekolah', - jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'], children, }: LayoutSekolahProps) { const router = useRouter(); const searchParams = useSearchParams(); - const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua'; + const snap = useSnapshot(infoSekolahPaud.jenjangPendidikan); - const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan); + const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState( + searchParams.get('jenjangPendidikan') || 'Semua' + ); - // Cleanup timeout + // Load jenjang pendidikan dari backend + useEffect(() => { + if (!snap.findMany.data) infoSekolahPaud.jenjangPendidikan.findMany.load(1, 100); + }, []); - - // Handle jenjang pendidikan click - const handleJenjangPendidikanChange = (k: string) => { - // arahkan langsung ke route jenjang pendidikan - if (k.toLowerCase() === 'semua') { - setJenjangPendidikanAktif(k); - router.push(`/darmasaba/pendidikan/info-sekolah/semua`); - } else { - setJenjangPendidikanAktif(k); - router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`); - } + const handleJenjangPendidikanChange = (nama: string) => { + setJenjangPendidikanAktif(nama); + const path = + nama.toLowerCase() === 'semua' + ? `/darmasaba/pendidikan/info-sekolah/semua` + : `/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(nama.toLowerCase())}`; + router.push(path); }; - + + // List tab dari data state + const jenjangList = ['Semua', ...(snap.findMany.data?.map((v) => v.nama) || [])]; return ( {/* Back Button */} - window.history.back()} variant="light" radius="md" size="lg"> - - Kembali - + {/* Search & Filter */} - {title} - - - Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu. + + {title} - - {jenjangPendidikanList.map((k) => { - const aktif = k === jenjangPendidikanAktif; - return ( - - ); - })} - + + + Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga + pengajar berdasarkan jenjang pendidikan (TK, SD, SMP, SMA). + + + {snap.findMany.loading ? ( + + + + ) : ( + + {jenjangList.map((k) => { + const aktif = k === jenjangPendidikanAktif; + return ( + + ); + })} + + )} - {/* Slot konten */} + {/* Konten anak */} {children} diff --git a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/semua/lembaga/page.tsx b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/semua/lembaga/page.tsx index d395cfd7..07bd3201 100644 --- a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/semua/lembaga/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/semua/lembaga/page.tsx @@ -47,7 +47,7 @@ function Page() { - Daftar Lembaga Pendidikan + Daftar Lembaga Pendidikan @@ -154,7 +159,7 @@ export default function SekolahPage() { aria-label="Tidak ada hasil" >
- + Tidak ditemukan @@ -212,7 +217,7 @@ export default function SekolahPage() { aria-hidden > {React.createElement(v.icon, { - color: '#2563eb', + color: colors['blue-button'], size: 34, stroke: 1.6, })} @@ -225,12 +230,12 @@ export default function SekolahPage() { {v.jumlah.toLocaleString()} - + {v.nama} - + @@ -244,8 +249,8 @@ export default function SekolahPage() { variant="outline" aria-label={`Lihat detail ${v.nama}`} style={{ - borderColor: '#e2e8f0', - color: '#2563eb', + borderColor: colors['blue-button'], + color: colors['blue-button'], paddingLeft: 20, paddingRight: 20, }} diff --git a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/semua/pengajar/page.tsx b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/semua/pengajar/page.tsx index 71150d79..60234dbf 100644 --- a/src/app/darmasaba/(pages)/pendidikan/info-sekolah/semua/pengajar/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/info-sekolah/semua/pengajar/page.tsx @@ -46,7 +46,7 @@ function Page() { - Daftar Pengajar + Daftar Pengajar - Daftar Siswa + Daftar Siswa - Bentuk pendidikan di luar sekolah yang terstruktur, bertujuan memberikan keterampilan, pengetahuan, dan pengembangan karakter masyarakat dari berbagai usia serta latar belakang. + Pendidikan non formal merupakan bentuk pendidikan di luar sekolah yang terstruktur, bertujuan untuk memberikan keterampilan, pengetahuan, serta pengembangan karakter masyarakat dari berbagai usia dan latar belakang. - - - <IconTarget size={28} style={{ marginRight: 8 }} /> - Tujuan Program - - - + + + + + + + + {stateTujuanPendidikanNonFormal.findById.data?.judul} + + + - - - <IconMapPin size={28} style={{ marginRight: 8 }} /> - Tempat Kegiatan - - - + + + + + + + + {stateTempatKegiatan.findById.data?.judul} + + + @@ -95,13 +103,17 @@ function Page() { withBorder > - - - <IconBook2 size={28} style={{ marginRight: 8 }} /> - Jenis Program yang Diselenggarakan - - - + + + + + + + + {stateJenisProgram.findById.data?.judul} + + + diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/[id]/page.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/[id]/page.tsx new file mode 100644 index 00000000..319f0d64 --- /dev/null +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/[id]/page.tsx @@ -0,0 +1,126 @@ +'use client'; + +import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital'; +import colors from '@/con/colors'; +import { + Badge, + Box, + Button, + Center, + Image, + Loader, + Paper, + Stack, + Text, + Title, +} from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowLeft, IconBook2 } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import ModalPeminjaman from '../../_lib/modalPeminjaman'; + +export default function DetailBukuUser() { + const router = useRouter(); + const params = useParams(); + const stateDetail = useProxy(perpustakaanDigitalState.dataPerpustakaan); + const [opened, setOpened] = useState(false); + + useShallowEffect(() => { + if (params?.id) stateDetail.findUnique.load(params.id as string); + }, [params?.id]); + + const data = stateDetail.findUnique.data; + + if (!data) { + return ( +
+ +
+ ); + } + + return ( + + {/* Tombol Kembali */} + + + + + {/* Cover Buku */} + {data.judul} + + {/* Judul & Kategori */} + + + {data.judul} + + {data.kategori?.name && ( + + {data.kategori.name} + + )} + + + {/* Deskripsi Buku */} + + + Deskripsi Buku + + + + + {/* Tombol Pinjam */} + + + + + {/* Modal Peminjaman */} + setOpened(false)} + buku={data} + /> + + ); +} diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/content.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/content.tsx index 9c41fb3d..349af199 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/content.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/[kategoriBuku]/content.tsx @@ -1,42 +1,52 @@ 'use client' import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital'; import colors from '@/con/colors'; -import { ActionIcon, Box, Center, Group, Image, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, Tooltip, Badge } from '@mantine/core'; +import { ActionIcon, Badge, Box, Button, Center, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconBook2, IconRefresh } from '@tabler/icons-react'; import { motion } from 'framer-motion'; +import { useTransitionRouter } from 'next-view-transitions'; import { useSearchParams } from 'next/navigation'; import { useCallback, useState } from 'react'; import { useProxy } from 'valtio/utils'; function Content({ kategoriBuku }: { kategoriBuku: string }) { const state = useProxy(perpustakaanDigitalState); - const [expandedId, setExpandedId] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); const searchParams = useSearchParams(); const searchQuery = searchParams.get('search') || ''; + const router = useTransitionRouter() const decodedKategoriBuku = decodeURIComponent(kategoriBuku); - const kategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku; - const loadData = useCallback(async (searchQuery: string = '') => { + const loadData = useCallback(async (searchQuery: string = '', page: number = 1) => { try { setIsLoading(true); - await state.dataPerpustakaan.findMany.load(1, 100, searchQuery, kategoriFilter); + const currentKategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku; + await state.dataPerpustakaan.findMany.load(page, 3, searchQuery, currentKategoriFilter); + setCurrentPage(page); + setTotalPages(state.dataPerpustakaan.findMany.totalPages); } finally { setIsLoading(false); } - }, [kategoriFilter, state.dataPerpustakaan.findMany]); + }, [state.dataPerpustakaan.findMany, decodedKategoriBuku]); useShallowEffect(() => { loadData(searchQuery); - }, [searchQuery, loadData]); + }, [searchQuery, loadData, kategoriBuku]); const handleRefresh = () => { loadData(); }; - if (isLoading || !state.dataPerpustakaan.findMany.load || !state.dataPerpustakaan.findMany.data) { + const handlePageChange = (newPage: number) => { + loadData(searchQuery, newPage); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + if ((isLoading && !state.dataPerpustakaan.findMany.data) || !state.dataPerpustakaan.findMany.load) { return ( @@ -57,18 +67,23 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) { Koleksi Buku
- - - - - + + + Halaman {currentPage} dari {totalPages} + + + + + + + {!state.dataPerpustakaan.findMany.data || state.dataPerpustakaan.findMany.data.length === 0 ? ( @@ -132,29 +147,23 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) { )} - - Lihat deskripsi - - } - hideLabel={ - - Sembunyikan deskripsi - - } - expanded={expandedId === v.id} - onExpandedChange={(isExpanded) => setExpandedId(isExpanded ? v.id : null)} - > - - + + {/* 📗 Tombol Detail */} + @@ -162,6 +171,15 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) { )} +
+ +
); } diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/layoutTabs.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/layoutTabs.tsx index b7b3d489..e61f85ca 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/layoutTabs.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/layoutTabs.tsx @@ -1,11 +1,24 @@ -'use client' -import { useEffect, useState } from 'react'; -import { ActionIcon, Box, Flex, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core'; -import { IconSearch, IconUser } from '@tabler/icons-react'; -import Link from 'next/link'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import BackButton from '../../../desa/layanan/_com/BackButto'; +'use client'; + +import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital'; import colors from '@/con/colors'; +import { + Box, + Grid, + GridCol, + Stack, + Tabs, + TabsList, + TabsTab, + Text, + TextInput +} from '@mantine/core'; +import { IconSearch } from '@tabler/icons-react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useSnapshot } from 'valtio'; +import BackButton from '../../../desa/layanan/_com/BackButto'; + type LayoutBukuProps = { placeholder?: string; @@ -15,7 +28,7 @@ type LayoutBukuProps = { children?: React.ReactNode; }; -function LayoutTabs({ +export default function LayoutTabs({ placeholder = 'Cari buku digital...', searchIcon = , children, @@ -23,6 +36,7 @@ function LayoutTabs({ const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const snap = useSnapshot(perpustakaanDigitalState); const activeTab = pathname.split('/').pop() || 'semua'; const initialSearch = searchParams.get('search') || ''; @@ -30,6 +44,11 @@ function LayoutTabs({ const [searchTimeout, setSearchTimeout] = useState(null); const [activeTabState, setActiveTabState] = useState(activeTab); + // 🟦 Ambil kategori buku saat mount + useEffect(() => { + perpustakaanDigitalState.kategoriBuku.findMany.load(); + }, []); + useEffect(() => { setActiveTabState(activeTab); }, [activeTab]); @@ -51,7 +70,9 @@ function LayoutTabs({ if (value) params.set('search', value); router.push( - `/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${params.toString() ? `?${params.toString()}` : ''}` + `/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${ + params.toString() ? `?${params.toString()}` : '' + }` ); }; @@ -63,41 +84,40 @@ function LayoutTabs({ } }; + // 🟩 Tabs dinamis berdasarkan kategori dari state + const kategoriTabs = + snap.kategoriBuku.findMany.data?.map((item) => ({ + label: item.name, + value: item.name.toLowerCase().replace(/\s+/g, '-'), + href: `/darmasaba/pendidikan/perpustakaan-digital/${encodeURIComponent(item.name.toLowerCase().replace(/\s+/g, '-'))}`, + })) ?? []; + const tabs = [ { label: 'Semua', value: 'semua', href: '/darmasaba/pendidikan/perpustakaan-digital/semua' }, - { label: 'Dokumenter', value: 'dokumenter', href: '/darmasaba/pendidikan/perpustakaan-digital/dokumenter' }, - { label: 'Sayuran', value: 'sayuran', href: '/darmasaba/pendidikan/perpustakaan-digital/sayuran' }, - { label: 'Dongeng', value: 'dongeng', href: '/darmasaba/pendidikan/perpustakaan-digital/dongeng' }, + ...kategoriTabs, ]; const handleTabChange = (value: string | null) => { if (!value) return; const params = new URLSearchParams(searchParams.toString()); - router.push(`/darmasaba/pendidikan/perpustakaan-digital/${value}${params.toString() ? `?${params.toString()}` : ''}`); + router.push( + `/darmasaba/pendidikan/perpustakaan-digital/${value}${ + params.toString() ? `?${params.toString()}` : '' + }` + ); }; return ( - - - - - Perpustakaan Digital Darmasaba + @@ -116,6 +136,7 @@ function LayoutTabs({ ))} + + {children} ); } - -export default LayoutTabs; diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx new file mode 100644 index 00000000..e137e07a --- /dev/null +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx @@ -0,0 +1,246 @@ +'use client'; + +import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; +import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital'; +import colors from '@/con/colors'; +import { + Badge, + Box, + Button, + Divider, + Group, + Image, + Modal, + Stack, + Text, + TextInput, +} from '@mantine/core'; +import { DateInput } from '@mantine/dates'; +import { IconArrowRight, IconBook2, IconUser } from '@tabler/icons-react'; +import { useEffect } from 'react'; +import { toast } from 'react-toastify'; +import { useSnapshot } from 'valtio'; + +export interface ModalPeminjamanProps { + opened: boolean; + onClose: () => void; + buku: { + id: string; + judul: string; + deskripsi?: string; + image?: { link?: string }; + kategori?: { name?: string }; + } | null; +} + +export default function ModalPeminjaman({ + opened, + onClose, + buku, +}: ModalPeminjamanProps) { + const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku); + + const BATAS_HARI_PINJAM = 4; + + // Reset form setiap modal dibuka + useEffect(() => { + if (opened && buku) { + perpustakaanDigitalState.peminjamanBuku.create.form = { + bukuId: buku.id, + nama: '', + noTelp: '', + alamat: '', + tanggalPinjam: '', + batasKembali: '', + tanggalKembali: '', + catatan: '', + }; + } + }, [opened, buku]); + + const handleSubmit = async () => { + if (!buku) return toast.error('Data buku tidak ditemukan'); + await perpustakaanDigitalState.peminjamanBuku.create.create(); + onClose(); + }; + + return ( + Formulir Peminjaman Buku
} + > + {buku ? ( + + {/* --- Info Buku --- */} + + {buku.judul} + + + + {buku.judul} + + + {buku.kategori?.name && ( + + {buku.kategori.name} + + )} + + + + + + + + {/* --- Form Input --- */} + } + value={snap.create.form.nama} + onChange={(e) => + (perpustakaanDigitalState.peminjamanBuku.create.form.nama = + e.currentTarget.value) + } + required + /> + + } + value={snap.create.form.noTelp} + onChange={(e) => + (perpustakaanDigitalState.peminjamanBuku.create.form.noTelp = + e.currentTarget.value) + } + required + /> + + } + value={snap.create.form.alamat} + onChange={(e) => + (perpustakaanDigitalState.peminjamanBuku.create.form.alamat = + e.currentTarget.value) + } + required + /> + + {/* === OTOMATIS SET BATAS DAN TANGGAL KEMBALI === */} + { + if (date) { + const tanggalPinjam = new Date(date); + + // simpan tanggal pinjam + perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = + tanggalPinjam.toISOString(); + + // hitung batas +4 hari + const batasKembali = new Date(tanggalPinjam); + batasKembali.setDate(batasKembali.getDate() + BATAS_HARI_PINJAM); + + // set batas & tanggal kembali otomatis + perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = + batasKembali.toISOString(); + perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali = + batasKembali.toISOString(); + + toast.info( + `Batas pengembalian otomatis diset ke ${batasKembali.toLocaleDateString('id-ID')} (+${BATAS_HARI_PINJAM} hari).` + ); + } else { + perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = ''; + perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = ''; + perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali = ''; + } + }} + required + /> + + + Catatan + + (perpustakaanDigitalState.peminjamanBuku.create.form.catatan = + val) + } + /> + + + + + + + + + ) : ( + + Tidak ada data buku yang dipilih + + )} + + ); +} diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/semua/content.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/semua/content.tsx index 40a98660..639bbed0 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/semua/content.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/semua/content.tsx @@ -1,45 +1,89 @@ -'use client' +/* eslint-disable react-hooks/exhaustive-deps */ +'use client'; + import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital'; import colors from '@/con/colors'; -import { Badge, Box, Center, Group, Image, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { motion } from 'framer-motion'; -import { useCallback, useState } from 'react'; -import { useProxy } from 'valtio/utils'; +import { + Badge, + Box, + Button, + Center, + Group, + Image, + Paper, + SimpleGrid, + Skeleton, + Stack, + Text, + Tooltip +} from '@mantine/core'; import { IconBook2, IconInfoCircle } from '@tabler/icons-react'; +import { Pagination } from '@mantine/core'; +import { motion } from 'framer-motion'; -type ContentProps = { - searchQuery: string; -}; +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; +import ModalPeminjaman from '../_lib/modalPeminjaman'; +import { useTransitionRouter } from 'next-view-transitions'; -function Content({ searchQuery }: ContentProps) { +export default function Content() { const state = useProxy(perpustakaanDigitalState); - const [expandedId, setExpandedId] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const router = useTransitionRouter() + const [opened, setOpened] = useState(false); + const searchParams = useSearchParams(); + const searchQuery = searchParams.get('search') || ''; + const [currentPage, setCurrentPage] = useState(1); + + const { data: books = [], loading, totalPages } = state.dataPerpustakaan.findMany; - const loadData = useCallback( - async (query: string = '') => { + const [selectedBook, setSelectedBook] = useState<{ + id: string; + judul: string; + deskripsi?: string; + image?: { link?: string }; + kategori?: { name?: string }; + } | null>(null); + + // Handle data loading and search + useEffect(() => { + let isMounted = true; + const controller = new AbortController(); + + const loadData = async () => { + if (!isMounted) return; + try { - setIsLoading(true); - await state.dataPerpustakaan.findMany.load(1, 100, query, ''); + await state.dataPerpustakaan.findMany.load( + currentPage, + 3, + searchQuery, + '' + ); } catch (error) { - console.error('Gagal memuat data:', error); - } finally { - setIsLoading(false); + if (!controller.signal.aborted) { + console.error('Gagal memuat data:', error); + } } - }, - [state.dataPerpustakaan.findMany] - ); + }; - useShallowEffect(() => { - loadData(searchQuery); - }, [searchQuery, loadData]); + const timer = setTimeout(loadData, 300); + + return () => { + isMounted = false; + controller.abort(); + clearTimeout(timer); + }; + }, [searchQuery, currentPage]); - if ( - isLoading || - !state.dataPerpustakaan.findMany.load || - !state.dataPerpustakaan.findMany.data - ) { + // Handle page change + const handlePageChange = (newPage: number) => { + setCurrentPage(newPage); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + // Loading state + if (loading || !books) { return ( @@ -53,6 +97,7 @@ function Content({ searchQuery }: ContentProps) { return ( + {/* 🔹 Header */} @@ -65,100 +110,152 @@ function Content({ searchQuery }: ContentProps) { - {!state.dataPerpustakaan.findMany.data || - state.dataPerpustakaan.findMany.data.length === 0 ? ( + {/* 📚 Empty State */} + {books.length === 0 ? (
- Kosong - Belum ada buku yang tersedia + Kosong + + Belum ada buku yang tersedia +
) : ( + // 📘 Daftar Buku - {state.dataPerpustakaan.findMany.data?.map((v, k) => ( + {books.map((v) => ( - - + + {/* 🖼 Gambar Buku */}
- {v.judul}
- - + {v.judul} {v.kategori && ( - + {v.kategori.name} )} - - Lihat deskripsi - - } - hideLabel={ - - Sembunyikan deskripsi - - } - expanded={expandedId === v.id} - onExpandedChange={(isExpanded) => setExpandedId(isExpanded ? v.id : null)} - > - - + + {/* 📝 Deskripsi */} +
+ + {/* 📗 Tombol Detail */} + + + {/* 📗 Tombol Peminjaman */} +
))}
)} + + +
+ +
+ + {/* 🔸 Modal Peminjaman */} + { + setOpened(false); + setSelectedBook(null); + }} + buku={selectedBook} + />
); } - -export default Content; diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/semua/page.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/semua/page.tsx index 0236b8cd..ab7bb140 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/semua/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/semua/page.tsx @@ -1,13 +1,13 @@ // src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx import { Suspense } from "react"; -import Content from "./content"; +import Content from "../[kategoriBuku]/content"; export default async function Page() { return ( Loading...
}> - + ); } \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/pendidikan/program-pendidikan-anak/page.tsx b/src/app/darmasaba/(pages)/pendidikan/program-pendidikan-anak/page.tsx index 3dcac57c..400e47e8 100644 --- a/src/app/darmasaba/(pages)/pendidikan/program-pendidikan-anak/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/program-pendidikan-anak/page.tsx @@ -37,13 +37,15 @@ function Page() { - + <Title ta="center" order={1} fw="bold" c={colors['blue-button']}> Program Pendidikan Anak + + + Desa Darmasaba berkomitmen mencetak generasi muda yang cerdas, berkarakter, dan siap bersaing melalui program pendidikan yang inklusif dan berkelanjutan. - - Tujuan Program + {stateTujuan.findById.data?.judul} - +
@@ -83,11 +85,11 @@ function Page() { - Program Unggulan + {stateUnggulan.findById.data?.judul} - + diff --git a/src/app/darmasaba/(pages)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/page.tsx b/src/app/darmasaba/(pages)/ppid/daftar-informasi-publik/[id]/page.tsx similarity index 97% rename from src/app/darmasaba/(pages)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/page.tsx rename to src/app/darmasaba/(pages)/ppid/daftar-informasi-publik/[id]/page.tsx index 42b22bec..23c2cee6 100644 --- a/src/app/darmasaba/(pages)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/daftar-informasi-publik/[id]/page.tsx @@ -118,6 +118,7 @@ export default function DetailInformasiPublikUser() { diff --git a/src/app/darmasaba/(pages)/ppid/daftar-informasi-publik-desa-darmasaba/page.tsx b/src/app/darmasaba/(pages)/ppid/daftar-informasi-publik/page.tsx similarity index 72% rename from src/app/darmasaba/(pages)/ppid/daftar-informasi-publik-desa-darmasaba/page.tsx rename to src/app/darmasaba/(pages)/ppid/daftar-informasi-publik/page.tsx index 1465f011..e5c33633 100644 --- a/src/app/darmasaba/(pages)/ppid/daftar-informasi-publik-desa-darmasaba/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/daftar-informasi-publik/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Center, + Group, Image, Pagination, Paper, @@ -68,16 +69,16 @@ function Page() { Logo Desa Darmasaba
- Daftar Informasi Publik Desa Darmasaba + Daftar Informasi Publik - + Tentang Informasi Publik - + Daftar Informasi Publik Desa Darmasaba adalah kumpulan data yang dapat diakses oleh masyarakat sesuai dengan ketentuan peraturan yang berlaku. @@ -117,41 +118,45 @@ function Page() { {(page - 1) * 5 + index + 1} - + - {item.jenisInformasi} + + {item.jenisInformasi} + - - + + - - {item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', { - day: '2-digit', - month: 'long', - year: 'numeric' - }) : '-'} + + + {item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric' + }) : '-'} + - + - - + color="blue" + leftSection={} + onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)} + > + Detail + + + ))} @@ -174,12 +179,18 @@ function Page() { Kontak PPID - - Email: ppid@desadarmasaba.id - - - WhatsApp: 081-xxx-xxx-xxx - + + + + Email: ppid@desadarmasaba.id + + + + + + WhatsApp: 081-xxx-xxx-xxx + + diff --git a/src/app/darmasaba/(pages)/ppid/dasar-hukum/page.tsx b/src/app/darmasaba/(pages)/ppid/dasar-hukum/page.tsx index fd0653b1..b9d64d1f 100644 --- a/src/app/darmasaba/(pages)/ppid/dasar-hukum/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/dasar-hukum/page.tsx @@ -42,7 +42,7 @@ function Page() { > Dasar Hukum - + Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum @@ -74,12 +74,11 @@ function Page() { fw="bold" fz={{ base: 'lg', md: 'xl' }} style={{ lineHeight: 1.4 }} - > - {item.judul} - + dangerouslySetInnerHTML={{ __html: item.judul }} + /> diff --git a/src/app/darmasaba/(pages)/ppid/ikm-desa-darmasaba/page.tsx b/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx similarity index 71% rename from src/app/darmasaba/(pages)/ppid/ikm-desa-darmasaba/page.tsx rename to src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx index ab39a966..8e8b40f7 100644 --- a/src/app/darmasaba/(pages)/ppid/ikm-desa-darmasaba/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx @@ -3,8 +3,8 @@ import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan"; import colors from "@/con/colors"; import { BarChart, PieChart } from '@mantine/charts'; -import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core"; -import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks"; +import { Box, Button, Center, Container, Flex, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core"; +import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useState } from "react"; import { useProxy } from "valtio/utils"; @@ -18,14 +18,13 @@ interface ChartDataItem { function Kepuasan() { - const state = useProxy(indeksKepuasanState.responden); +const state = useProxy(indeksKepuasanState.responden); const { data, loading } = state.findMany; const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState([]); const [donutDataRating, setDonutDataRating] = useState([]); const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState([]); - const [barChartData, setBarChartData] = useState>([]); - const [opened, { open, close }] = useDisclosure(false); - const isMobile = useMediaQuery("(max-width: 768px)"); + const [barChartData, setBarChartData] = useState>([]); + const [opened, { open, close }] = useDisclosure(false) const resetForm = () => { state.create.form = { @@ -42,7 +41,7 @@ function Kepuasan() { indeksKepuasanState.jenisKelaminResponden.findMany.load() indeksKepuasanState.pilihanRatingResponden.findMany.load() indeksKepuasanState.kelompokUmurResponden.findMany.load() - }) + },[]) const handleSubmit = async () => { try { @@ -122,18 +121,18 @@ function Kepuasan() { // Convert map to array and sort by date const barData = Array.from(monthYearMap.entries()) - .map(([key, count]) => { + .map(([key, Responden]) => { const [year, month] = key.split('-'); const monthName = new Date(Number(year), Number(month) - 1, 1) .toLocaleString('id-ID', { month: 'long' }); return { month: `${monthName} ${year}`, - count, + Responden, sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10) }; }) .sort((a, b) => a.sortKey - b.sortKey) - .map(({ month, count }) => ({ month, count })); + .map(({ month, Responden }) => ({ month, Responden })); setBarChartData(barData); } @@ -141,12 +140,12 @@ function Kepuasan() { if ((loading && !data) || !data) { return ( - - - - - - + + + + + + ); @@ -157,10 +156,16 @@ function Kepuasan() {
- 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!
- +
@@ -177,10 +182,10 @@ function Kepuasan() { {/* Chart Jenis Kelamin */} @@ -215,7 +217,7 @@ function Kepuasan() { withLabels withTooltip labelsType="percent" - size={200} + size={250} // Fixed size in pixels data={donutDataJenisKelamin} />
@@ -237,7 +239,7 @@ function Kepuasan() { {/* Chart Rating */} - Pilihan + Ulasan {donutDataRating.every(item => item.value === 0) ? ( Belum ada data untuk ditampilkan dalam grafik @@ -254,7 +256,7 @@ function Kepuasan() { labelsPosition="outside" labelsType="percent" withLabelsLine - size={200} + size={250} data={donutDataRating} /> @@ -297,7 +299,7 @@ function Kepuasan() { labelsPosition="outside" labelsType="percent" withLabelsLine - size={190} + size={250} data={donutDataKelompokUmur} /> @@ -330,8 +332,8 @@ function Kepuasan() { { state.create.form.name = val.currentTarget.value; }} @@ -340,7 +342,7 @@ function Kepuasan() { label="Tanggal" type="date" placeholder="masukkan tanggal" - defaultValue={state.create.form.tanggal} + value={state.create.form.tanggal} onChange={(val) => { state.create.form.tanggal = val.currentTarget.value; }} @@ -349,7 +351,7 @@ function Kepuasan() { key={"jenisKelamin"} label={"Jenis Kelamin"} placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'} - defaultValue={state.create.form.jenisKelaminId || ""} + value={state.create.form.jenisKelaminId || ""} onChange={(val) => { state.create.form.jenisKelaminId = val ?? ""; }} @@ -367,7 +369,7 @@ function Kepuasan() { key={"rating_responden"} label={"Rating"} placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'} - defaultValue={state.create.form.ratingId || ""} + value={state.create.form.ratingId || ""} onChange={(val) => { state.create.form.ratingId = val ?? ""; }} @@ -385,7 +387,7 @@ function Kepuasan() { key={"kelompokUmur"} label={"Kelompok Umur"} placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'} - defaultValue={state.create.form.kelompokUmurId || ""} + value={state.create.form.kelompokUmurId || ""} onChange={(val) => { state.create.form.kelompokUmurId = val ?? ""; }} @@ -413,41 +415,57 @@ 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! +
+ +
- - - - - - Pelayanan Terhadap Publik Desa Darmasaba - - Total Responden - - {state.findMany.total.toLocaleString("id-ID")} + + + + + + + Pelayanan Terhadap Publik Desa Darmasaba + + + Total Responden + + {state.findMany.total.toLocaleString('id-ID')} - - + + {/* Chart Jenis Kelamin */} @@ -457,17 +475,28 @@ function Kepuasan() { Belum ada data untuk ditampilkan dalam grafik ) : ( - - -
- -
-
+ + + +
+ +
+
+ + {donutDataJenisKelamin.map((entry) => ( + + + {entry.name}: {entry.value} + + ))} + +
)}
@@ -476,24 +505,41 @@ function Kepuasan() { {/* Chart Rating */} - Pilihan + Ulasan {donutDataRating.every(item => item.value === 0) ? ( Belum ada data untuk ditampilkan dalam grafik ) : ( - - -
- -
-
+ + + +
+ +
+
+ + + {donutDataRating.map((entry) => ( + + + + {entry.name}: {entry.value} + + + ))} + + +
)}
@@ -508,18 +554,35 @@ function Kepuasan() { Belum ada data untuk ditampilkan dalam grafik
) : ( - - -
- -
-
+ + + +
+ +
+
+ + + {donutDataKelompokUmur.map((entry) => ( + + + + {entry.name}: {entry.value} + + + ))} + + +
)}
@@ -542,7 +605,7 @@ function Kepuasan() { }} /> ({ value: item.id, diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx index 66068037..5a47ba67 100644 --- a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx @@ -12,10 +12,9 @@ import { SimpleGrid, Stack, Text, - TextInput, - Tooltip, + TextInput } from '@mantine/core'; -import { IconDownload, IconSend2 } from '@tabler/icons-react'; +import { IconSend2 } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; @@ -150,23 +149,6 @@ function Page() {
))} - -
- - - -
-