Merge branch 'nico/28-nov-25' into staging
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -41,6 +41,9 @@ next-env.d.ts
|
|||||||
# uploads
|
# uploads
|
||||||
/uploads
|
/uploads
|
||||||
|
|
||||||
|
# download
|
||||||
|
/download
|
||||||
|
|
||||||
# cache
|
# cache
|
||||||
/cache
|
/cache
|
||||||
|
|
||||||
|
|||||||
23
package.json
23
package.json
@@ -3,9 +3,9 @@
|
|||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --bun next dev --hostname 0.0.0.0",
|
"dev": "next dev",
|
||||||
"build": "bun --bun next build",
|
"build": "next build",
|
||||||
"start": "bun --bun next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "bun run prisma/seed.ts"
|
"seed": "bun run prisma/seed.ts"
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"@elysiajs/static": "^1.3.0",
|
"@elysiajs/static": "^1.3.0",
|
||||||
"@elysiajs/stream": "^1.1.0",
|
"@elysiajs/stream": "^1.1.0",
|
||||||
"@elysiajs/swagger": "^1.2.0",
|
"@elysiajs/swagger": "^1.2.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/carousel": "^7.16.2",
|
"@mantine/carousel": "^7.16.2",
|
||||||
"@mantine/charts": "^7.17.1",
|
"@mantine/charts": "^7.17.1",
|
||||||
"@mantine/core": "^7.17.4",
|
"@mantine/core": "^7.17.4",
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"@mantine/dropzone": "^8.1.1",
|
"@mantine/dropzone": "^8.1.1",
|
||||||
"@mantine/form": "^8.1.0",
|
"@mantine/form": "^8.1.0",
|
||||||
"@mantine/hooks": "^7.17.4",
|
"@mantine/hooks": "^7.17.4",
|
||||||
|
"@mantine/modals": "^8.3.6",
|
||||||
"@mantine/tiptap": "^7.17.4",
|
"@mantine/tiptap": "^7.17.4",
|
||||||
"@paljs/types": "^8.1.0",
|
"@paljs/types": "^8.1.0",
|
||||||
"@prisma/client": "^6.3.1",
|
"@prisma/client": "^6.3.1",
|
||||||
@@ -39,19 +41,27 @@
|
|||||||
"@tiptap/pm": "^2.11.7",
|
"@tiptap/pm": "^2.11.7",
|
||||||
"@tiptap/react": "^2.11.7",
|
"@tiptap/react": "^2.11.7",
|
||||||
"@tiptap/starter-kit": "^2.11.7",
|
"@tiptap/starter-kit": "^2.11.7",
|
||||||
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.2",
|
||||||
"@types/leaflet": "^1.9.20",
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
|
"@types/nodemailer": "^7.0.2",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"bun": "^1.2.2",
|
"bun": "^1.2.2",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
"colors": "^1.4.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
"embla-carousel-autoplay": "^8.5.2",
|
"embla-carousel": "^8.6.0",
|
||||||
"embla-carousel-react": "^7.1.0",
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"extract-zip": "^2.0.1",
|
||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
"framer-motion": "^12.23.5",
|
"framer-motion": "^12.23.5",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
@@ -67,17 +77,20 @@
|
|||||||
"next": "^15.5.2",
|
"next": "^15.5.2",
|
||||||
"next-view-transitions": "^0.3.4",
|
"next-view-transitions": "^0.3.4",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"nodemailer": "^7.0.10",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.6",
|
"primereact": "^10.9.6",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "^6.3.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-exif-orientation-img": "^0.1.5",
|
||||||
"react-international-phone": "^4.6.0",
|
"react-international-phone": "^4.6.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-simple-toasts": "^6.1.0",
|
"react-simple-toasts": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"readdirp": "^4.1.1",
|
"readdirp": "^4.1.1",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
[
|
[
|
||||||
{ "name": "Semua" },
|
|
||||||
{ "name": "Pemerintahan" },
|
{ "name": "Pemerintahan" },
|
||||||
{ "name": "Pembangunan" },
|
{ "name": "Pembangunan" },
|
||||||
{ "name": "Ekonomi" },
|
{ "name": "Ekonomi" },
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "edit",
|
||||||
"name": "Pelayanan Penduduk Non-Permanent",
|
"name": "Pelayanan Penduduk Non-Permanent",
|
||||||
"deskripsi": "<p>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.</p>"
|
"deskripsi": "<p>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.</p>"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "edit",
|
||||||
"name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)",
|
"name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)",
|
||||||
"deskripsi": "<p>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.</p>",
|
"deskripsi": "<p>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.</p>",
|
||||||
"link" : "https://oss.go.id/"
|
"link" : "https://oss.go.id/"
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
91
prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json
Normal file
91
prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@@ -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",
|
"id": "cmds9023u0008vnbe3oxmhwyf",
|
||||||
"name": "Desa Darmasaba",
|
"name": "Desa Darmasaba",
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{ "nama": "Kebersihan" },
|
||||||
|
{ "nama": "Infrastruktur" },
|
||||||
|
{ "nama": "Sosial" },
|
||||||
|
{ "nama": "Lingkungan" }
|
||||||
|
]
|
||||||
@@ -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" }
|
||||||
|
]
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
"id": "cmgewz4gt000704ib91i3f169",
|
||||||
"namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.",
|
"namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.",
|
||||||
"gelarAkademik": "S.H.,M.H.,NL.P.",
|
"gelarAkademik": "S.H.,M.H.,NL.P.",
|
||||||
"tanggalMasuk": "2020-01-01T00:00:00.000Z",
|
"tanggalMasuk": "2020-01-01T00:00:00.000Z",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"isActive": true
|
"isActive": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440002",
|
"id": "cmgewxfvw000004ibee5013f4",
|
||||||
"namaLengkap": "I Ketut Suwanta",
|
"namaLengkap": "I Ketut Suwanta",
|
||||||
"gelarAkademik": "S.Pt",
|
"gelarAkademik": "S.Pt",
|
||||||
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"isActive": true
|
"isActive": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440006",
|
"id": "cmgewxvqw000104ibgm5l8fzs",
|
||||||
"namaLengkap": "Ni Wayan Supardiati",
|
"namaLengkap": "Ni Wayan Supardiati",
|
||||||
"gelarAkademik": "S.Pd",
|
"gelarAkademik": "S.Pd",
|
||||||
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"isActive": true
|
"isActive": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440011",
|
"id": "cmgewy1g9000204ib2n7hbx0i",
|
||||||
"namaLengkap": "I Wayan Agus Juni Artha Saputra",
|
"namaLengkap": "I Wayan Agus Juni Artha Saputra",
|
||||||
"gelarAkademik": "S.T.",
|
"gelarAkademik": "S.T.",
|
||||||
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"isActive": true
|
"isActive": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440012",
|
"id": "cmgewybah000304ibgqhn1gm2",
|
||||||
"namaLengkap": "I Wayan Sueca",
|
"namaLengkap": "I Wayan Sueca",
|
||||||
"gelarAkademik": "S.H.",
|
"gelarAkademik": "S.H.",
|
||||||
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"isActive": true
|
"isActive": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440017",
|
"id": "cmgewygqz000404ib20sv8nvg",
|
||||||
"namaLengkap": "Si Gede Ketut Astawa",
|
"namaLengkap": "Si Gede Ketut Astawa",
|
||||||
"gelarAkademik": "S.T.",
|
"gelarAkademik": "S.T.",
|
||||||
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"isActive": true
|
"isActive": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440018",
|
"id": "cmgewyos1000504ibcu8o2gyk",
|
||||||
"namaLengkap": "I Kadek Arya Minarta",
|
"namaLengkap": "I Kadek Arya Minarta",
|
||||||
"gelarAkademik": "S.T.",
|
"gelarAkademik": "S.T.",
|
||||||
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"isActive": true
|
"isActive": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440021",
|
"id": "cmgewyxk7000604ib8djs3i6c",
|
||||||
"namaLengkap": "I Gede Andika Pradnya Diputra",
|
"namaLengkap": "I Gede Andika Pradnya Diputra",
|
||||||
"gelarAkademik": "S.E.",
|
"gelarAkademik": "S.E.",
|
||||||
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"name": "DEVELOPER",
|
||||||
|
"description": "Developer",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "1",
|
||||||
"name": "ADMIN DESA",
|
"name": "SUPER ADMIN",
|
||||||
"description": "Administrator Desa",
|
"description": "Administrator",
|
||||||
"permissions": ["manage_users", "manage_content", "view_reports"],
|
"isActive": true
|
||||||
"isActive": true,
|
|
||||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
|
||||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2",
|
"id": "2",
|
||||||
"name": "ADMIN KESEHATAN",
|
"name": "ADMIN DESA",
|
||||||
"description": "Administrator Bidang Kesehatan",
|
"description": "Administrator Desa",
|
||||||
"permissions": ["manage_health_data", "view_reports"],
|
"isActive": true
|
||||||
"isActive": true,
|
|
||||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
|
||||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"name": "ADMIN SEKOLAH",
|
"name": "ADMIN KESEHATAN",
|
||||||
"description": "Administrator Sekolah",
|
"description": "Administrator Bidang Kesehatan",
|
||||||
"permissions": ["manage_school_data", "view_reports"],
|
"isActive": true
|
||||||
"isActive": true,
|
},
|
||||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
{
|
||||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
"id": "4",
|
||||||
|
"name": "ADMIN PENDIDIKAN",
|
||||||
|
"description": "Administrator Bidang Pendidikan",
|
||||||
|
"isActive": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,32 +1,10 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "cmie1o0zh0002vn132vtzg7hh",
|
||||||
"nama": "Admin Desa",
|
"username": "SuperAdmin-Nico",
|
||||||
"nomor": "089647037426",
|
"nomor": "6289647037426",
|
||||||
"roleId": "1",
|
"roleId": 0,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"lastLogin": "2025-08-31T10:00:00.000Z",
|
"sessionInvalid": false
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
1127
prisma/migrations/20251119062255_add_unique_username/migration.sql
Normal file
1127
prisma/migrations/20251119062255_add_unique_username/migration.sql
Normal file
File diff suppressed because it is too large
Load Diff
30
prisma/safeseedUnique.ts
Normal file
30
prisma/safeseedUnique.ts
Normal file
@@ -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<T extends keyof PrismaClient>(
|
||||||
|
model: T,
|
||||||
|
where: Record<string, any>,
|
||||||
|
data: Record<string, any>
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ model FileStorage {
|
|||||||
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
|
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
|
||||||
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
|
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
|
||||||
PasarDesa PasarDesa[]
|
PasarDesa PasarDesa[]
|
||||||
Pegawai Pegawai[]
|
PegawaiBumDes PegawaiBumDes[]
|
||||||
DesaDigital DesaDigital[]
|
DesaDigital DesaDigital[]
|
||||||
InfoTekno InfoTekno[]
|
InfoTekno InfoTekno[]
|
||||||
PengaduanMasyarakat PengaduanMasyarakat[]
|
PengaduanMasyarakat PengaduanMasyarakat[]
|
||||||
@@ -101,6 +101,7 @@ model FileStorage {
|
|||||||
MitraKolaborasi MitraKolaborasi[]
|
MitraKolaborasi MitraKolaborasi[]
|
||||||
|
|
||||||
ArtikelKesehatan ArtikelKesehatan[]
|
ArtikelKesehatan ArtikelKesehatan[]
|
||||||
|
StrukturBumDes StrukturBumDes[]
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU LANDING PAGE ========================================= //
|
//========================================= MENU LANDING PAGE ========================================= //
|
||||||
@@ -142,7 +143,7 @@ model MediaSosial {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= PROFILE ========================================= //
|
//========================================= DESA ANTI KORUPSI ========================================= //
|
||||||
model DesaAntiKorupsi {
|
model DesaAntiKorupsi {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
@@ -183,18 +184,46 @@ model SdgsDesa {
|
|||||||
//========================================= APBDes ========================================= //
|
//========================================= APBDes ========================================= //
|
||||||
model APBDes {
|
model APBDes {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
tahun Int?
|
||||||
jumlah String
|
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])
|
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
|
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
|
||||||
fileId String?
|
fileId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime? // opsional, tidak perlu default now()
|
||||||
isActive Boolean @default(true)
|
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 ========================================= //
|
//========================================= PRESTASI DESA ========================================= //
|
||||||
model PrestasiDesa {
|
model PrestasiDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -298,6 +327,7 @@ model PosisiOrganisasiPPID {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model PegawaiPPID {
|
model PegawaiPPID {
|
||||||
@@ -316,15 +346,16 @@ model PegawaiPPID {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
||||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||||
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model StrukturOrganisasiPPID {
|
model StrukturOrganisasiPPID {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
posisiOrganisasiId String @db.VarChar(50)
|
posisiOrganisasiId String @db.VarChar(50)
|
||||||
pegawaiId String @db.Uuid
|
pegawaiId String
|
||||||
hubunganOrganisasiId String @db.Uuid
|
hubunganOrganisasiId String
|
||||||
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
|
posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id])
|
||||||
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
|
pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
@@ -1167,6 +1198,7 @@ model KontakDarurat {
|
|||||||
deskripsi String
|
deskripsi String
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String
|
||||||
|
whatsapp String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
@@ -1340,6 +1372,7 @@ model PasarDesa {
|
|||||||
harga Int
|
harga Int
|
||||||
rating Float
|
rating Float
|
||||||
alamatUsaha String
|
alamatUsaha String
|
||||||
|
kontak String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
@@ -1382,6 +1415,7 @@ model LowonganPekerjaan {
|
|||||||
gaji String
|
gaji String
|
||||||
deskripsi String
|
deskripsi String
|
||||||
kualifikasi String
|
kualifikasi String
|
||||||
|
notelp String
|
||||||
tanggalPosting DateTime @default(now())
|
tanggalPosting DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -1391,24 +1425,39 @@ model LowonganPekerjaan {
|
|||||||
|
|
||||||
// ========================================= STRUKTUR ORGANISASI ========================================= //
|
// ========================================= STRUKTUR ORGANISASI ========================================= //
|
||||||
|
|
||||||
model PosisiOrganisasi {
|
model StrukturBumDes {
|
||||||
id String @id @default(uuid()) @db.VarChar(50)
|
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 PosisiOrganisasiBumDes {
|
||||||
|
id String @id @default(cuid())
|
||||||
nama String @db.VarChar(100)
|
nama String @db.VarChar(100)
|
||||||
deskripsi String? @db.Text
|
deskripsi String? @db.Text
|
||||||
hierarki Int
|
hierarki Int
|
||||||
|
pegawai PegawaiBumDes[]
|
||||||
pegawai Pegawai[]
|
strukturOrganisasi StrukturBumDes[] // Relasi balik
|
||||||
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
parentId String?
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
parent PosisiOrganisasiBumDes? @relation("Parent", fields: [parentId], references: [id])
|
||||||
@@map("posisi_organisasi")
|
children PosisiOrganisasiBumDes[] @relation("Parent")
|
||||||
|
StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Pegawai {
|
model PegawaiBumDes {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(cuid())
|
||||||
namaLengkap String @db.VarChar(255)
|
namaLengkap String @db.VarChar(255)
|
||||||
gelarAkademik String? @db.VarChar(100)
|
gelarAkademik String? @db.VarChar(100)
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
@@ -1421,49 +1470,22 @@ model Pegawai {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
posisi PosisiOrganisasiBumDes @relation(fields: [posisiId], references: [id])
|
||||||
posisi PosisiOrganisasi @relation(fields: [posisiId], references: [id])
|
strukturOrganisasi StrukturBumDes[] // Relasi balik
|
||||||
|
StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
|
||||||
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
|
|
||||||
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
|
|
||||||
|
|
||||||
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
|
||||||
|
|
||||||
@@map("pegawai")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model HubunganOrganisasi {
|
model StrukturOrganisasiBumDes {
|
||||||
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 StrukturOrganisasi {
|
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
posisiOrganisasiId String @db.VarChar(50)
|
posisiOrganisasiId String @db.VarChar(50)
|
||||||
pegawaiId String @db.Uuid
|
pegawaiId String
|
||||||
hubunganOrganisasiId String @db.Uuid
|
hubunganOrganisasiId String
|
||||||
|
posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id])
|
||||||
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
|
pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id])
|
||||||
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
|
|
||||||
hubunganOrganisasi HubunganOrganisasi @relation(fields: [hubunganOrganisasiId], references: [id])
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
@@map("struktur_organisasi")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= PROGRAM KEMISKINAN ========================================= //
|
// ========================================= PROGRAM KEMISKINAN ========================================= //
|
||||||
@@ -1612,7 +1634,7 @@ model Pembiayaan {
|
|||||||
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
|
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= INOVASI ========================================= //
|
// ========================================= MENU INOVASI ========================================= //
|
||||||
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
|
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
|
||||||
model DesaDigital {
|
model DesaDigital {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -1950,17 +1972,22 @@ model KeunggulanProgram {
|
|||||||
model BeasiswaPendaftar {
|
model BeasiswaPendaftar {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
namaLengkap String
|
namaLengkap String
|
||||||
nik String @unique
|
nis String?
|
||||||
|
kelas String?
|
||||||
|
jenisKelamin JenisKelamin
|
||||||
|
alamatDomisili String?
|
||||||
tempatLahir String
|
tempatLahir String
|
||||||
tanggalLahir DateTime
|
tanggalLahir DateTime
|
||||||
jenisKelamin JenisKelamin
|
namaOrtu String?
|
||||||
kewarganegaraan String
|
nik String @unique
|
||||||
agama Agama
|
pekerjaanOrtu String?
|
||||||
alamatKTP String
|
penghasilan String?
|
||||||
alamatDomisili String?
|
|
||||||
noHp String
|
noHp String
|
||||||
email String @unique
|
kewarganegaraan String?
|
||||||
statusPernikahan StatusPernikahan
|
agama Agama?
|
||||||
|
alamatKTP String?
|
||||||
|
email String? @unique
|
||||||
|
statusPernikahan StatusPernikahan?
|
||||||
ukuranBaju UkuranBaju?
|
ukuranBaju UkuranBaju?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -2093,6 +2120,9 @@ model DataPerpustakaan {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
// relasi baru ke peminjaman
|
||||||
|
peminjamanBuku PeminjamanBuku[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model KategoriBuku {
|
model KategoriBuku {
|
||||||
@@ -2105,28 +2135,56 @@ model KategoriBuku {
|
|||||||
DataPerpustakaan DataPerpustakaan[]
|
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 ========================================= //
|
// ========================================= USER ========================================= //
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
username String
|
username String
|
||||||
nomor String @unique
|
nomor String @unique
|
||||||
role Role @relation(fields: [roleId], references: [id])
|
roleId String @default("2")
|
||||||
roleId String @default("1")
|
isActive Boolean @default(false)
|
||||||
instansi String?
|
sessionInvalid Boolean @default(false)
|
||||||
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
lastLogin DateTime?
|
lastLogin DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
deletedAt DateTime?
|
permissions Json?
|
||||||
|
sessions UserSession[] // ✅ Relasi one-to-many
|
||||||
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
|
menuAccesses UserMenuAccess[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
|
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
|
||||||
description String?
|
description String?
|
||||||
permissions Json // Menyimpan permission dalam format JSON
|
permissions Json?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -2145,26 +2203,32 @@ model KodeOtp {
|
|||||||
otp Int
|
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
|
|
||||||
|
|
||||||
@@map("permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
model UserSession {
|
model UserSession {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
token String
|
token String @db.Text // ✅ JWT bisa panjang
|
||||||
expires DateTime?
|
expiresAt DateTime // ✅ Ubah jadi expiresAt (konsisten)
|
||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
User User @relation(fields: [userId], references: [id])
|
|
||||||
userId String @unique
|
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 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 ========================================= //
|
// ========================================= DATA PENDIDIKAN ========================================= //
|
||||||
|
|||||||
249
prisma/seed.ts
249
prisma/seed.ts
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import profilePejabatDesa from "./data/landing-page/profile/profile.json";
|
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 visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
|
||||||
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
|
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
|
||||||
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.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-bumdes.json";
|
||||||
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
|
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
|
||||||
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
|
import kategoriBerita from "./data/desa/berita/kategori-berita.json";
|
||||||
import kategoriBerita from "./data/kategori-berita.json";
|
|
||||||
import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.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 materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
|
||||||
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
||||||
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.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 filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
|
||||||
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
||||||
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
|
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
|
||||||
@@ -54,69 +55,87 @@ import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-progr
|
|||||||
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||||
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||||
import roles from "./data/user/roles.json";
|
import roles from "./data/user/roles.json";
|
||||||
import users from "./data/user/users.json";
|
|
||||||
import fileStorage from "./data/file-storage.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 () => {
|
(async () => {
|
||||||
// =========== USER & ROLE ===========
|
|
||||||
// In your seed.ts
|
|
||||||
// =========== ROLES ===========
|
|
||||||
console.log("🔄 Seeding 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...");
|
console.log("🔄 Seeding users...");
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
// First verify the role exists
|
try {
|
||||||
|
// Verify role exists first
|
||||||
const roleExists = await prisma.role.findUnique({
|
const roleExists = await prisma.role.findUnique({
|
||||||
where: { id: u.roleId },
|
where: { id: u.roleId.toString() },
|
||||||
|
select: { id: true }, // Only select id to minimize query
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!roleExists) {
|
if (!roleExists) {
|
||||||
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
|
console.error(
|
||||||
|
`❌ Role with id ${u.roleId} not found for user ${u.username}`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.user.upsert({
|
await safeSeedUnique(
|
||||||
where: { id: u.id },
|
"user",
|
||||||
update: {
|
{ id: u.id },
|
||||||
username: u.nama,
|
{
|
||||||
|
username: u.username,
|
||||||
nomor: u.nomor,
|
nomor: u.nomor,
|
||||||
roleId: u.roleId,
|
roleId: u.roleId.toString(),
|
||||||
isActive: u.isActive,
|
isActive: u.isActive,
|
||||||
},
|
sessionInvalid: false,
|
||||||
create: {
|
|
||||||
id: u.id,
|
|
||||||
username: u.nama,
|
|
||||||
nomor: u.nomor,
|
|
||||||
roleId: u.roleId,
|
|
||||||
isActive: u.isActive,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
console.log("✅ Users seeded");
|
);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("✅ Users seeding completed");
|
||||||
|
|
||||||
// =========== FILE STORAGE ===========
|
// =========== FILE STORAGE ===========
|
||||||
console.log("🔄 Seeding file storage...");
|
console.log("🔄 Seeding file storage...");
|
||||||
for (const f of fileStorage) {
|
for (const f of fileStorage) {
|
||||||
|
try {
|
||||||
await prisma.fileStorage.upsert({
|
await prisma.fileStorage.upsert({
|
||||||
where: { id: f.id },
|
where: { id: f.id },
|
||||||
update: {
|
update: {
|
||||||
@@ -137,6 +156,9 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
category: f.category,
|
category: f.category,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ Failed to seed file storage ${f.name}:`, error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log("✅ File storage seeded");
|
console.log("✅ File storage seeded");
|
||||||
// =========== LANDING PAGE ===========
|
// =========== LANDING PAGE ===========
|
||||||
@@ -364,6 +386,7 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
jumlah: l.jumlah,
|
jumlah: l.jumlah,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
|
id: l.id,
|
||||||
name: l.name,
|
name: l.name,
|
||||||
jumlah: l.jumlah,
|
jumlah: l.jumlah,
|
||||||
},
|
},
|
||||||
@@ -554,15 +577,40 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
console.log("posisi organisasi berhasil");
|
console.log("posisi organisasi berhasil");
|
||||||
|
|
||||||
// =========== PEGAWAI PPID ===========
|
// =========== PEGAWAI PPID ===========
|
||||||
|
console.log("🔄 Seeding pegawai PPID...");
|
||||||
const flattenedPegawai = pegawaiPPID.flat();
|
const flattenedPegawai = pegawaiPPID.flat();
|
||||||
|
|
||||||
|
// Check for duplicate emails
|
||||||
|
const emails = new Set();
|
||||||
for (const p of flattenedPegawai) {
|
for (const p of flattenedPegawai) {
|
||||||
|
if (emails.has(p.email)) {
|
||||||
|
console.warn(`⚠️ Duplicate email found in pegawaiPPID: ${p.email}`);
|
||||||
|
}
|
||||||
|
emails.add(p.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of flattenedPegawai) {
|
||||||
|
try {
|
||||||
await prisma.pegawaiPPID.upsert({
|
await prisma.pegawaiPPID.upsert({
|
||||||
where: { id: p.id },
|
where: { id: p.id },
|
||||||
update: p,
|
update: p,
|
||||||
create: 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 berhasil");
|
}
|
||||||
|
}
|
||||||
|
console.log("✅ pegawai PPID seeding completed");
|
||||||
|
|
||||||
// =========== SUBMENU VISI MISI PPID ===========
|
// =========== SUBMENU VISI MISI PPID ===========
|
||||||
|
|
||||||
@@ -823,28 +871,36 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
}
|
}
|
||||||
console.log("kategori produk success ...");
|
console.log("kategori produk success ...");
|
||||||
|
|
||||||
for (const p of posisiOrganisasi) {
|
const flattenedPosisiBumdes = posisiOrganisasi.flat();
|
||||||
await prisma.posisiOrganisasi.upsert({
|
|
||||||
where: {
|
// ✅ Urutkan berdasarkan hierarki
|
||||||
id: p.id,
|
const sortedPosisiBumdes = flattenedPosisiBumdes.sort(
|
||||||
},
|
(a, b) => a.hierarki - b.hierarki
|
||||||
update: {
|
);
|
||||||
nama: p.nama,
|
|
||||||
deskripsi: p.deskripsi,
|
for (const p of sortedPosisiBumdes) {
|
||||||
hierarki: p.hierarki,
|
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
||||||
},
|
|
||||||
create: {
|
if (p.parentId) {
|
||||||
id: p.id,
|
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
|
||||||
nama: p.nama,
|
if (!parentExists) {
|
||||||
deskripsi: p.deskripsi,
|
console.warn(
|
||||||
hierarki: p.hierarki,
|
`⚠️ 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) {
|
for (const p of pegawai) {
|
||||||
await prisma.pegawai.upsert({
|
await prisma.pegawaiBumDes.upsert({
|
||||||
where: {
|
where: {
|
||||||
id: p.id,
|
id: p.id,
|
||||||
},
|
},
|
||||||
@@ -873,26 +929,6 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
}
|
}
|
||||||
console.log("pegawai success ...");
|
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) {
|
for (const d of detailDataPengangguran) {
|
||||||
await prisma.detailDataPengangguran.upsert({
|
await prisma.detailDataPengangguran.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -916,6 +952,30 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
}
|
}
|
||||||
console.log("📊 detailDataPengangguran success ...");
|
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) {
|
for (const e of tujuanEdukasiLingkungan) {
|
||||||
await prisma.tujuanEdukasiLingkungan.upsert({
|
await prisma.tujuanEdukasiLingkungan.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -1169,6 +1229,25 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
console.log(
|
console.log(
|
||||||
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
|
"✅ 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())
|
.then(() => prisma.$disconnect())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
118
prisma/seed_assets.ts
Normal file
118
prisma/seed_assets.ts
Normal file
@@ -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<string[]> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
public/beasiswa-siswa.png
Normal file
BIN
public/beasiswa-siswa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 446 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 378 KiB |
@@ -7,6 +7,7 @@ import Underline from '@tiptap/extension-underline';
|
|||||||
import TextAlign from '@tiptap/extension-text-align';
|
import TextAlign from '@tiptap/extension-text-align';
|
||||||
import Superscript from '@tiptap/extension-superscript';
|
import Superscript from '@tiptap/extension-superscript';
|
||||||
import SubScript from '@tiptap/extension-subscript';
|
import SubScript from '@tiptap/extension-subscript';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
type CreateEditorProps = {
|
type CreateEditorProps = {
|
||||||
value: string;
|
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 (
|
return (
|
||||||
<RichTextEditor editor={editor}>
|
<RichTextEditor editor={editor}>
|
||||||
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">
|
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) {
|
|||||||
};
|
};
|
||||||
}, [editor, onChange]);
|
}, [editor, onChange]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RichTextEditor editor={editor}>
|
<RichTextEditor editor={editor}>
|
||||||
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">
|
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
IconAmbulance,
|
IconAmbulance,
|
||||||
IconCash,
|
IconCash,
|
||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
IconTrophy,
|
IconTrophy,
|
||||||
IconTruckFilled,
|
IconTruckFilled,
|
||||||
IconBuilding,
|
IconBuilding,
|
||||||
IconAlertTriangle
|
IconAlertTriangle,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
@@ -57,7 +57,7 @@ const iconMap = {
|
|||||||
darurat: { label: 'Darurat', icon: IconAlertTriangle },
|
darurat: { label: 'Darurat', icon: IconAlertTriangle },
|
||||||
};
|
};
|
||||||
|
|
||||||
type IconKey = keyof typeof iconMap;
|
export type IconKey = keyof typeof iconMap;
|
||||||
|
|
||||||
const iconList = Object.entries(iconMap).map(([value, data]) => ({
|
const iconList = Object.entries(iconMap).map(([value, data]) => ({
|
||||||
value,
|
value,
|
||||||
@@ -67,44 +67,52 @@ const iconList = Object.entries(iconMap).map(([value, data]) => ({
|
|||||||
export default function SelectIconProgramEdit({
|
export default function SelectIconProgramEdit({
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
|
...props
|
||||||
}: {
|
}: {
|
||||||
onChange: (value: IconKey) => void;
|
onChange: (value: IconKey | '') => void;
|
||||||
value: IconKey;
|
value: IconKey | '';
|
||||||
}) {
|
} & Omit<SelectProps, 'onChange' | 'value' | 'data'>) {
|
||||||
const IconComponent = iconMap[value]?.icon || null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box maw={300}>
|
<Box maw={300}>
|
||||||
<Select
|
<Select
|
||||||
placeholder="Pilih ikon"
|
placeholder="Pilih ikon"
|
||||||
value={value}
|
value={value || ''}
|
||||||
onChange={(value) => {
|
onChange={(val: string | null) => {
|
||||||
if (value) onChange(value as IconKey);
|
if (val) {
|
||||||
|
onChange(val as IconKey);
|
||||||
|
} else {
|
||||||
|
onChange('');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
data={iconList}
|
data={iconList}
|
||||||
|
renderOption={({ option }) => {
|
||||||
|
const Icon = iconMap[option.value as IconKey]?.icon;
|
||||||
|
return (
|
||||||
|
<Group gap="sm">
|
||||||
|
{Icon && <Icon size={18} stroke={1.5} />}
|
||||||
|
{option.label}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}}
|
||||||
leftSection={
|
leftSection={
|
||||||
IconComponent && (
|
value && iconMap[value as IconKey] ? (
|
||||||
<Box>
|
<Box ml={-4}>
|
||||||
<IconComponent size={24} stroke={1.5} />
|
{(() => {
|
||||||
|
const Icon = iconMap[value as IconKey].icon;
|
||||||
|
return <Icon size={20} stroke={1.5} />;
|
||||||
|
})()}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
) : null
|
||||||
}
|
}
|
||||||
withCheckIcon={false}
|
searchable
|
||||||
searchable={false}
|
|
||||||
rightSectionWidth={0}
|
|
||||||
styles={{
|
styles={{
|
||||||
input: {
|
input: {
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: rem(16),
|
|
||||||
paddingLeft: 40,
|
paddingLeft: 40,
|
||||||
},
|
fontSize: rem(16),
|
||||||
section: {
|
|
||||||
left: 10,
|
|
||||||
right: 'auto',
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ const berita = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
berita.findMany.loading = true; // ✅ Akses langsung via nama path
|
const startTime = Date.now();
|
||||||
|
berita.findMany.loading = true;
|
||||||
berita.findMany.page = page;
|
berita.findMany.page = page;
|
||||||
berita.findMany.search = search;
|
berita.findMany.search = search;
|
||||||
|
|
||||||
@@ -98,7 +99,14 @@ const berita = proxy({
|
|||||||
berita.findMany.data = [];
|
berita.findMany.data = [];
|
||||||
berita.findMany.totalPages = 1;
|
berita.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
|
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const minDelay = 300;
|
||||||
|
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
berita.findMany.loading = false;
|
berita.findMany.loading = false;
|
||||||
|
}, delay);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -581,33 +581,24 @@ const pelayananPerizinanBerusaha = proxy({
|
|||||||
findById: {
|
findById: {
|
||||||
data: null as pelayananPerizinanBerusahaForm | null,
|
data: null as pelayananPerizinanBerusahaForm | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
initialize() {
|
|
||||||
pelayananPerizinanBerusaha.findById.data = {
|
|
||||||
id: "",
|
|
||||||
name: "",
|
|
||||||
deskripsi: "",
|
|
||||||
link: "",
|
|
||||||
} as pelayananPerizinanBerusahaForm;
|
|
||||||
},
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
pelayananPerizinanBerusaha.findById.loading = true;
|
this.loading = true;
|
||||||
const res = await fetch(
|
const response = await fetch(`/api/desa/layanan/pelayananperizinanberusaha/${id}`);
|
||||||
`/api/desa/layanan/pelayananperizinanberusaha/${id}`
|
if (!response.ok) {
|
||||||
);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
pelayananPerizinanBerusaha.findById.data = data.data ?? null;
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Failed to fetch pelayanan perizinan berusaha:",
|
|
||||||
res.statusText
|
|
||||||
);
|
|
||||||
pelayananPerizinanBerusaha.findById.data = null;
|
|
||||||
}
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
if (result?.success) {
|
||||||
|
this.data = result.data; // Make sure this matches your API response structure
|
||||||
|
}
|
||||||
|
return result?.data || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pelayanan perizinan berusaha:", error);
|
console.error('Error loading data:', error);
|
||||||
pelayananPerizinanBerusaha.findById.data = null;
|
toast.error('Gagal memuat data');
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const penghargaanState = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
penghargaanState.findMany.load();
|
penghargaanState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ const pengumuman = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
pengumuman.findMany.load();
|
pengumuman.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -101,6 +101,38 @@ const ApbDesa = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findFirst: {
|
||||||
|
data: null as Prisma.ApbDesaGetPayload<{
|
||||||
|
include: { pendapatan: true; belanja: true; pembiayaan: true };
|
||||||
|
}> | null,
|
||||||
|
loading: false,
|
||||||
|
async load(params?: Record<string, any>) {
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
// ✅ request ke endpoint find-first
|
||||||
|
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
|
||||||
|
"find-first"
|
||||||
|
].get({ query: params || {} });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
this.data = res.data.data ?? null;
|
||||||
|
} else {
|
||||||
|
this.data = null;
|
||||||
|
toast.error(res.data?.message || "Gagal memuat data pertama APB Desa");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error findFirst APB Desa:", error);
|
||||||
|
toast.error("Gagal memuat data APB Desa pertama");
|
||||||
|
this.data = null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.data = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
update: {
|
update: {
|
||||||
id: "",
|
id: "",
|
||||||
form: { ...ApbDesaDefaultForm },
|
form: { ...ApbDesaDefaultForm },
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const demografiPekerjaan = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
demografiPekerjaan.create.form = { ...defaultForm };
|
demografiPekerjaan.create.form = { ...defaultForm };
|
||||||
demografiPekerjaan.findMany.load();
|
demografiPekerjaan.findMany.load();
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const jumlahPendudukMiskin = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
jumlahPendudukMiskin.create.form = {
|
jumlahPendudukMiskin.create.form = {
|
||||||
year: 0,
|
year: 0,
|
||||||
totalPoorPopulation: 0,
|
totalPoorPopulation: 0,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const jumlahPengangguran = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.id;
|
const id = res.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
|
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
|
||||||
jumlahPengangguran.findMany.load();
|
jumlahPengangguran.findMany.load();
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const templateForm = z.object({
|
|||||||
gaji: z.string(),
|
gaji: z.string(),
|
||||||
deskripsi: z.string(),
|
deskripsi: z.string(),
|
||||||
kualifikasi: z.string(),
|
kualifikasi: z.string(),
|
||||||
|
notelp: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
@@ -23,6 +24,7 @@ const defaultForm = {
|
|||||||
gaji: "",
|
gaji: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
kualifikasi: "",
|
kualifikasi: "",
|
||||||
|
notelp: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const lowonganKerjaState = proxy({
|
const lowonganKerjaState = proxy({
|
||||||
@@ -45,7 +47,7 @@ const lowonganKerjaState = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
lowonganKerjaState.create.loading = false;
|
lowonganKerjaState.create.loading = false;
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
@@ -179,6 +181,7 @@ const lowonganKerjaState = proxy({
|
|||||||
gaji: data.gaji,
|
gaji: data.gaji,
|
||||||
deskripsi: data.deskripsi,
|
deskripsi: data.deskripsi,
|
||||||
kualifikasi: data.kualifikasi,
|
kualifikasi: data.kualifikasi,
|
||||||
|
notelp: data.notelp,
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@@ -218,6 +221,7 @@ const lowonganKerjaState = proxy({
|
|||||||
gaji: this.form.gaji,
|
gaji: this.form.gaji,
|
||||||
deskripsi: this.form.deskripsi,
|
deskripsi: this.form.deskripsi,
|
||||||
kualifikasi: this.form.kualifikasi,
|
kualifikasi: this.form.kualifikasi,
|
||||||
|
notelp: this.form.notelp,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const templatePasarDesaForm = z.object({
|
|||||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
rating: z.number().min(1, "Rating minimal 1"),
|
rating: z.number().min(1, "Rating minimal 1"),
|
||||||
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
|
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
|
||||||
|
kontak: z.string().min(1, "Kontak wajib diisi"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultPasarDesaForm = {
|
const defaultPasarDesaForm = {
|
||||||
@@ -21,6 +22,7 @@ const defaultPasarDesaForm = {
|
|||||||
imageId: "",
|
imageId: "",
|
||||||
rating: 0,
|
rating: 0,
|
||||||
kategoriId: [] as string[],
|
kategoriId: [] as string[],
|
||||||
|
kontak: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const pasarDesa = proxy({
|
const pasarDesa = proxy({
|
||||||
@@ -188,6 +190,7 @@ const pasarDesa = proxy({
|
|||||||
imageId: data.imageId,
|
imageId: data.imageId,
|
||||||
rating: data.rating,
|
rating: data.rating,
|
||||||
kategoriId: data.kategoriId,
|
kategoriId: data.kategoriId,
|
||||||
|
kontak: data.kontak,
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@@ -225,6 +228,7 @@ const pasarDesa = proxy({
|
|||||||
imageId: this.form.imageId,
|
imageId: this.form.imageId,
|
||||||
rating: this.form.rating,
|
rating: this.form.rating,
|
||||||
kategoriId: this.form.kategoriId,
|
kategoriId: this.form.kategoriId,
|
||||||
|
kontak: this.form.kontak,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -336,6 +340,40 @@ const kategoriProduk = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// ✅ Versi findManyAll (ambil semua tanpa pagination)
|
||||||
|
findManyAll: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.KategoriProdukGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (search = "") => {
|
||||||
|
kategoriProduk.findManyAll.loading = true;
|
||||||
|
kategoriProduk.findManyAll.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = {};
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many-all"].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriProduk.findManyAll.data = res.data.data ?? [];
|
||||||
|
} else {
|
||||||
|
kategoriProduk.findManyAll.data = [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori produk (all):", err);
|
||||||
|
kategoriProduk.findManyAll.data = [];
|
||||||
|
} finally {
|
||||||
|
kategoriProduk.findManyAll.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.KategoriProdukGetPayload<{
|
data: null as Prisma.KategoriProdukGetPayload<{
|
||||||
omit: { isActive: true };
|
omit: { isActive: true };
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const programKemiskinanState = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
programKemiskinanState.findMany.load();
|
programKemiskinanState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const grafikSektorUnggulan = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
grafikSektorUnggulan.create.form = {
|
grafikSektorUnggulan.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
|||||||
@@ -1,9 +1,173 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { proxy } from "valtio";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const templateForm = z.object({
|
||||||
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultForm = {
|
||||||
|
name: "",
|
||||||
|
imageId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type StrukturBumDesForm = Prisma.StrukturBumDesGetPayload<{
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
name: true;
|
||||||
|
imageId: true;
|
||||||
|
image?: {
|
||||||
|
select: {
|
||||||
|
link: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const stateStruktur = proxy({
|
||||||
|
struktur: {
|
||||||
|
data: null as StrukturBumDesForm | null,
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/ekonomi/struktur-organisasi/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.data = result.data;
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal mengambil data struktur");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
|
this.error = errorMessage;
|
||||||
|
console.error("Load struktur error:", errorMessage);
|
||||||
|
toast.error("Terjadi kesalahan saat mengambil data struktur");
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
editStruktur: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
isReadOnly: false,
|
||||||
|
|
||||||
|
initialize(strukturData: StrukturBumDesForm) {
|
||||||
|
this.id = strukturData.id;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
this.form = {
|
||||||
|
name: strukturData.name || "",
|
||||||
|
imageId: strukturData.imageId || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateField(field: keyof typeof defaultForm, value: string) {
|
||||||
|
this.form[field] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const validation = templateForm.safeParse(this.form);
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
const errors = validation.error.issues
|
||||||
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||||
|
.join(", ");
|
||||||
|
toast.error(`Form tidak valid: ${errors}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/ekonomi/struktur-organisasi/${this.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(
|
||||||
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Berhasil update struktur");
|
||||||
|
await stateStruktur.struktur.load(this.id);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update struktur");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
|
this.error = errorMessage;
|
||||||
|
console.error("Update struktur error:", errorMessage);
|
||||||
|
toast.error("Terjadi kesalahan saat update struktur");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.id = "";
|
||||||
|
this.form = { ...defaultForm };
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadForEdit(id: string) {
|
||||||
|
const strukturData = await this.struktur.load(id);
|
||||||
|
if (strukturData) {
|
||||||
|
this.editStruktur.initialize(strukturData);
|
||||||
|
}
|
||||||
|
return strukturData;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.struktur.reset();
|
||||||
|
this.editStruktur.reset();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const templatePosisiOrganisasi = z.object({
|
const templatePosisiOrganisasi = z.object({
|
||||||
nama: z.string().min(1, "Nama harus diisi"),
|
nama: z.string().min(1, "Nama harus diisi"),
|
||||||
@@ -30,9 +194,7 @@ const posisiOrganisasi = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
|
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['create'].post(this.form);
|
||||||
"posisi-organisasi"
|
|
||||||
]["create"].post(this.form);
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Berhasil menambahkan posisi organisasi");
|
toast.success("Berhasil menambahkan posisi organisasi");
|
||||||
posisiOrganisasi.findMany.load();
|
posisiOrganisasi.findMany.load();
|
||||||
@@ -52,6 +214,29 @@ const posisiOrganisasi = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.StrukturOrganisasiBumDesGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/ekonomi/struktur-organisasi/posisi-organisasi/${id}`
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
posisiOrganisasi.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch posisiOrganisasi:", res.statusText);
|
||||||
|
posisiOrganisasi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching posisiOrganisasi:", error);
|
||||||
|
posisiOrganisasi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
edit: {
|
edit: {
|
||||||
id: "",
|
id: "",
|
||||||
form: { ...posisiOrganisasiDefaultForm },
|
form: { ...posisiOrganisasiDefaultForm },
|
||||||
@@ -165,16 +350,16 @@ const posisiOrganisasi = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => {
|
load: async (page = 1, limit?: number, search = "") => {
|
||||||
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
|
const appliedLimit = limit ?? 10;
|
||||||
posisiOrganisasi.findMany.page = page;
|
posisiOrganisasi.findMany.page = page;
|
||||||
posisiOrganisasi.findMany.search = search;
|
posisiOrganisasi.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: any = { page, limit: appliedLimit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
|
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many'].get({ query });
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
posisiOrganisasi.findMany.data = res.data.data ?? [];
|
posisiOrganisasi.findMany.data = res.data.data ?? [];
|
||||||
@@ -192,7 +377,42 @@ const posisiOrganisasi = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findManyAll: {
|
||||||
|
data: [] as Array<{
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
deskripsi: string | null;
|
||||||
|
hierarki: number;
|
||||||
|
}>,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
|
||||||
|
posisiOrganisasi.findManyAll.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { search };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many-all'].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
posisiOrganisasi.findManyAll.data = res.data.data || [];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load posisiOrganisasi:", res.data?.message);
|
||||||
|
posisiOrganisasi.findManyAll.data = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading posisiOrganisasi:", error);
|
||||||
|
posisiOrganisasi.findManyAll.data = [];
|
||||||
|
} finally {
|
||||||
|
posisiOrganisasi.findManyAll.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
delete: {
|
delete: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async byId(id: string) {
|
async byId(id: string) {
|
||||||
@@ -231,12 +451,12 @@ const posisiOrganisasi = proxy({
|
|||||||
|
|
||||||
const templatePegawai = z.object({
|
const templatePegawai = z.object({
|
||||||
namaLengkap: z.string().min(1, "Nama wajib diisi"),
|
namaLengkap: z.string().min(1, "Nama wajib diisi"),
|
||||||
gelarAkademik: z.string().optional(),
|
gelarAkademik: z.string().min(1, "Gelar Akademik wajib diisi"),
|
||||||
imageId: z.string().nullable().optional(),
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
tanggalMasuk: z.string().optional(), // ISO format
|
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ISO format
|
||||||
email: z.string().email("Email tidak valid").optional(),
|
email: z.string().email("Email tidak valid").optional(),
|
||||||
telepon: z.string().optional(),
|
telepon: z.string().min(1, "Telepom wajib diisi"),
|
||||||
alamat: z.string().optional(),
|
alamat: z.string().min(1, "Alamat wajib diisi"),
|
||||||
posisiId: z.string().min(1, "Posisi wajib diisi"),
|
posisiId: z.string().min(1, "Posisi wajib diisi"),
|
||||||
isActive: z.boolean().default(true),
|
isActive: z.boolean().default(true),
|
||||||
});
|
});
|
||||||
@@ -267,9 +487,9 @@ const pegawai = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
pegawai.create.loading = true;
|
pegawai.create.loading = true;
|
||||||
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
|
const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['create'].post(
|
||||||
"pegawai"
|
pegawai.create.form
|
||||||
]["create"].post(pegawai.create.form);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Pegawai berhasil ditambahkan");
|
toast.success("Pegawai berhasil ditambahkan");
|
||||||
await pegawai.findMany.load();
|
await pegawai.findMany.load();
|
||||||
@@ -287,19 +507,30 @@ const pegawai = proxy({
|
|||||||
|
|
||||||
// In struktur-organisasi.ts
|
// In struktur-organisasi.ts
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as any[] | null,
|
data: null as
|
||||||
|
| Prisma.PegawaiBumDesGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
posisi: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
page: 1,
|
page: 1,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
pegawai.findMany.loading = true; // Use the full path to access the property
|
pegawai.findMany.loading = true; // Use the full path to access the property
|
||||||
pegawai.findMany.page = page;
|
pegawai.findMany.page = page;
|
||||||
|
pegawai.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
|
const query: any = { page, limit };
|
||||||
"pegawai"
|
if (search) query.search = search;
|
||||||
]["find-many"].get({
|
|
||||||
query: { page, limit },
|
const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['find-many'].get({
|
||||||
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
@@ -324,7 +555,7 @@ findMany: {
|
|||||||
},
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as
|
data: null as
|
||||||
| (Prisma.PegawaiGetPayload<{
|
| (Prisma.PegawaiBumDesGetPayload<{
|
||||||
include: { posisi: true; image: true };
|
include: { posisi: true; image: true };
|
||||||
}> & { isActive: boolean })
|
}> & { isActive: boolean })
|
||||||
| null,
|
| null,
|
||||||
@@ -350,12 +581,9 @@ findMany: {
|
|||||||
if (!id) return toast.warn("ID tidak valid");
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
try {
|
try {
|
||||||
pegawai.delete.loading = true;
|
pegawai.delete.loading = true;
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`, {
|
||||||
`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`,
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}
|
});
|
||||||
);
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success(json.message ?? "Berhasil hapus pegawai");
|
toast.success(json.message ?? "Berhasil hapus pegawai");
|
||||||
@@ -372,6 +600,31 @@ findMany: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
nonActive: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
try {
|
||||||
|
pegawai.nonActive.loading = true;
|
||||||
|
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/non-active/${id}`, {
|
||||||
|
method: "DELETE", // biasanya nonActive pakai PATCH
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
|
||||||
|
await pegawai.findMany.load(); // refresh data
|
||||||
|
} else {
|
||||||
|
toast.error(json.message ?? "Gagal menonaktifkan pegawai");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal nonActive:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
|
||||||
|
} finally {
|
||||||
|
pegawai.nonActive.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
edit: {
|
edit: {
|
||||||
id: "",
|
id: "",
|
||||||
form: { ...pegawaiDefaultForm },
|
form: { ...pegawaiDefaultForm },
|
||||||
@@ -384,15 +637,12 @@ findMany: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`, {
|
||||||
`/api/ekonomi/struktur-organisasi/pegawai/${id}`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
@@ -503,299 +753,10 @@ findMany: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schema Zod untuk form validasi
|
const stateStrukturBumDes = proxy({
|
||||||
const templateHubunganOrganisasiForm = z.object({
|
stateStruktur,
|
||||||
atasanId: z.string().min(1, "Atasan wajib dipilih"),
|
|
||||||
bawahanId: z.string().min(1, "Bawahan wajib dipilih"),
|
|
||||||
tipe: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Default form state
|
|
||||||
const defaultHubunganOrganisasiForm = {
|
|
||||||
atasanId: "",
|
|
||||||
bawahanId: "",
|
|
||||||
tipe: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// ====================== STATE ===========================
|
|
||||||
const hubunganOrganisasi = proxy({
|
|
||||||
create: {
|
|
||||||
form: { ...defaultHubunganOrganisasiForm },
|
|
||||||
loading: false,
|
|
||||||
async create() {
|
|
||||||
const cek = templateHubunganOrganisasiForm.safeParse(
|
|
||||||
hubunganOrganisasi.create.form
|
|
||||||
);
|
|
||||||
if (!cek.success) {
|
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}: ${v.message}`)
|
|
||||||
.join("\n")}]`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
hubunganOrganisasi.create.loading = true;
|
|
||||||
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
|
|
||||||
"hubungan-organisasi"
|
|
||||||
]["create"].post(hubunganOrganisasi.create.form);
|
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
|
||||||
hubunganOrganisasi.findMany.load();
|
|
||||||
return toast.success("Berhasil menambahkan hubungan organisasi");
|
|
||||||
} else {
|
|
||||||
return toast.error(res.data?.message || "Gagal menambahkan data");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Gagal create:", error);
|
|
||||||
toast.error("Terjadi kesalahan saat menambahkan");
|
|
||||||
} finally {
|
|
||||||
hubunganOrganisasi.create.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
findMany: {
|
|
||||||
data: null as Array<{
|
|
||||||
id: string;
|
|
||||||
atasanId: string;
|
|
||||||
bawahanId: string;
|
|
||||||
tipe?: string | null;
|
|
||||||
atasan: {
|
|
||||||
id: string;
|
|
||||||
namaLengkap: string;
|
|
||||||
gelarAkademik: string | null;
|
|
||||||
imageId: string | null;
|
|
||||||
tanggalMasuk: Date | null;
|
|
||||||
email: string | null;
|
|
||||||
telepon: string | null;
|
|
||||||
alamat: string | null;
|
|
||||||
posisiId: string;
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
bawahan: {
|
|
||||||
id: string;
|
|
||||||
namaLengkap: string;
|
|
||||||
gelarAkademik: string | null;
|
|
||||||
imageId: string | null;
|
|
||||||
tanggalMasuk: Date | null;
|
|
||||||
email: string | null;
|
|
||||||
telepon: string | null;
|
|
||||||
alamat: string | null;
|
|
||||||
posisiId: string;
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
}> | null,
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
try {
|
|
||||||
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
|
|
||||||
"hubungan-organisasi"
|
|
||||||
]["find-many"].get();
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
hubunganOrganisasi.findMany.data = (res.data?.data ?? []).map(
|
|
||||||
(item: any) => ({
|
|
||||||
...item,
|
|
||||||
atasan: item.atasan
|
|
||||||
? {
|
|
||||||
...item.atasan,
|
|
||||||
isActive: item.atasan.isActive ?? item.atasan.aktif ?? true,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
bawahan: item.bawahan
|
|
||||||
? {
|
|
||||||
...item.bawahan,
|
|
||||||
isActive:
|
|
||||||
item.bawahan.isActive ?? item.bawahan.aktif ?? true,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
hubunganOrganisasi.findMany.data = [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Fetch list error:", error);
|
|
||||||
toast.error("Gagal memuat data hubungan organisasi");
|
|
||||||
hubunganOrganisasi.findMany.data = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
findUnique: {
|
|
||||||
data: null as {
|
|
||||||
id: string;
|
|
||||||
atasanId: string;
|
|
||||||
bawahanId: string;
|
|
||||||
tipe?: string | null;
|
|
||||||
atasan?: {
|
|
||||||
id: string;
|
|
||||||
namaLengkap: string;
|
|
||||||
gelarAkademik: string | null;
|
|
||||||
imageId: string;
|
|
||||||
tanggalMasuk: Date | null;
|
|
||||||
email: string | null;
|
|
||||||
telepon: string | null;
|
|
||||||
alamat: string | null;
|
|
||||||
posisiId: string;
|
|
||||||
aktif: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
bawahan?: {
|
|
||||||
id: string;
|
|
||||||
namaLengkap: string;
|
|
||||||
gelarAkademik: string | null;
|
|
||||||
imageId: string;
|
|
||||||
tanggalMasuk: Date | null;
|
|
||||||
email: string | null;
|
|
||||||
telepon: string | null;
|
|
||||||
alamat: string | null;
|
|
||||||
posisiId: string;
|
|
||||||
aktif: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
} | null,
|
|
||||||
|
|
||||||
async load(id: string) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
|
|
||||||
);
|
|
||||||
const result = await res.json();
|
|
||||||
|
|
||||||
if (res.ok && result?.success) {
|
|
||||||
hubunganOrganisasi.findUnique.data = result.data;
|
|
||||||
} else {
|
|
||||||
hubunganOrganisasi.findUnique.data = null;
|
|
||||||
toast.error(result?.message || "Gagal mengambil data");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Find unique error:", error);
|
|
||||||
hubunganOrganisasi.findUnique.data = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
edit: {
|
|
||||||
id: "",
|
|
||||||
form: { ...defaultHubunganOrganisasiForm },
|
|
||||||
loading: false,
|
|
||||||
|
|
||||||
async load(id: string) {
|
|
||||||
if (!id) return toast.warn("ID tidak valid");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
|
|
||||||
);
|
|
||||||
const result = await res.json();
|
|
||||||
|
|
||||||
if (res.ok && result?.success) {
|
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
|
||||||
this.form = {
|
|
||||||
atasanId: data.atasanId,
|
|
||||||
bawahanId: data.bawahanId,
|
|
||||||
tipe: data.tipe || "",
|
|
||||||
};
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
throw new Error(result?.message || "Gagal memuat data");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading:", error);
|
|
||||||
toast.error(
|
|
||||||
error instanceof Error ? error.message : "Gagal memuat data"
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async update() {
|
|
||||||
const cek = templateHubunganOrganisasiForm.safeParse(this.form);
|
|
||||||
if (!cek.success) {
|
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}: ${v.message}`)
|
|
||||||
.join("\n")}]`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.loading = true;
|
|
||||||
const res = await fetch(
|
|
||||||
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${this.id}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(this.form),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await res.json();
|
|
||||||
if (res.ok && result.success) {
|
|
||||||
await hubunganOrganisasi.findMany.load();
|
|
||||||
toast.success("Berhasil mengupdate hubungan organisasi");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(result?.message || "Gagal mengupdate");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Update error:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Gagal update");
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
hubunganOrganisasi.edit.id = "";
|
|
||||||
hubunganOrganisasi.edit.form = { ...defaultHubunganOrganisasiForm };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: {
|
|
||||||
loading: false,
|
|
||||||
async byId(id: string) {
|
|
||||||
if (!id) return toast.warn("ID tidak valid");
|
|
||||||
|
|
||||||
try {
|
|
||||||
hubunganOrganisasi.delete.loading = true;
|
|
||||||
const res = await fetch(
|
|
||||||
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/del/${id}`,
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await res.json();
|
|
||||||
if (res.ok && result?.success) {
|
|
||||||
toast.success("Hubungan organisasi berhasil dihapus");
|
|
||||||
hubunganOrganisasi.findMany.load();
|
|
||||||
} else {
|
|
||||||
toast.error(result?.message || "Gagal menghapus hubungan organisasi");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Delete error:", error);
|
|
||||||
toast.error("Terjadi kesalahan saat menghapus");
|
|
||||||
} finally {
|
|
||||||
hubunganOrganisasi.delete.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const strukturorganisasiState = proxy({
|
|
||||||
posisiOrganisasi,
|
posisiOrganisasi,
|
||||||
pegawai,
|
pegawai,
|
||||||
hubunganOrganisasi,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default strukturorganisasiState;
|
export default stateStrukturBumDes;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
grafikBerdasarkanUsiaKerjaNganggur.create.form = {
|
grafikBerdasarkanUsiaKerjaNganggur.create.form = {
|
||||||
usia18_25: "",
|
usia18_25: "",
|
||||||
usia26_35: "",
|
usia26_35: "",
|
||||||
@@ -255,7 +255,7 @@ const grafikBerdasarkanPendidikan = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
grafikBerdasarkanPendidikan.create.form = {
|
grafikBerdasarkanPendidikan.create.form = {
|
||||||
SD: "",
|
SD: "",
|
||||||
SMP: "",
|
SMP: "",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const desaDigitalState = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
desaDigitalState.findMany.load();
|
desaDigitalState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const infoTeknoState = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
infoTeknoState.findMany.load();
|
infoTeknoState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { proxy } from "valtio";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(1, "Nama minimal 1 karakter"),
|
name: z.string().min(5, "Nama minimal 5 karakter"),
|
||||||
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
deskripsi: z.string().min(5, "Deskripsi minimal 5 karakter"),
|
||||||
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
|
slug: z.string().min(5, "Deskripsi singkat minimal 5 karakter"),
|
||||||
icon: z.string().min(1, "Icon minimal 1 karakter"),
|
icon: z.string().min(1, "Icon minimal 1 karakter"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ const programKreatifState = proxy({
|
|||||||
const err = `[${cek.error.issues
|
const err = `[${cek.error.issues
|
||||||
.map((v) => `${v.path.join(".")}`)
|
.map((v) => `${v.path.join(".")}`)
|
||||||
.join("\n")}] required`;
|
.join("\n")}] required`;
|
||||||
return toast.error(err);
|
toast.error(err);
|
||||||
|
return false; // ⬅️ ini penting
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -37,18 +38,24 @@ const programKreatifState = proxy({
|
|||||||
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
|
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
|
||||||
programKreatifState.create.form
|
programKreatifState.create.form
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
programKreatifState.findMany.load();
|
programKreatifState.findMany.load();
|
||||||
return toast.success("success create");
|
toast.success("Sukses menambahkan");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
console.log(res);
|
|
||||||
return toast.error("failed create");
|
toast.error("failed create");
|
||||||
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log((error as Error).message);
|
console.error((error as Error).message);
|
||||||
|
toast.error("Terjadi kesalahan saat create");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
programKreatifState.create.loading = false;
|
programKreatifState.create.loading = false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as any[] | null,
|
data: null as any[] | null,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const keamananLingkunganState = proxy({
|
|||||||
].post(keamananLingkunganState.create.form);
|
].post(keamananLingkunganState.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
keamananLingkunganState.findMany.load();
|
keamananLingkunganState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const kontakDaruratKeamananState = proxy({
|
|||||||
].post(kontakDaruratKeamananState.create.form);
|
].post(kontakDaruratKeamananState.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
kontakDaruratKeamananState.findMany.load();
|
kontakDaruratKeamananState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
@@ -294,7 +294,7 @@ const kontakDaruratItem = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
kontakDaruratItem.findMany.load();
|
kontakDaruratItem.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const laporanPublikState = proxy({
|
|||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
laporanPublikState.findMany.load();
|
laporanPublikState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(res);
|
console.log(res);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const pencegahanKriminalitasState = proxy({
|
|||||||
].post(pencegahanKriminalitasState.create.form);
|
].post(pencegahanKriminalitasState.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
pencegahanKriminalitasState.findMany.load();
|
pencegahanKriminalitasState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const tipsKeamananState = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
tipsKeamananState.findMany.load();
|
tipsKeamananState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ const dokter = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data;
|
const id = res.data?.data;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
dokter.create.create.form = { ...defaultDokterForm };
|
dokter.create.create.form = { ...defaultDokterForm };
|
||||||
dokter.findMany.load();
|
dokter.findMany.load();
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const grafikkepuasan = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data;
|
const id = res.data?.data;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
grafikkepuasan.create.form = { ...defaultForm };
|
grafikkepuasan.create.form = { ...defaultForm };
|
||||||
grafikkepuasan.findMany.load();
|
grafikkepuasan.findMany.load();
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const persentasekelahiran = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data;
|
const id = res.data?.data;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
persentasekelahiran.create.form = { ...defaultForm };
|
persentasekelahiran.create.form = { ...defaultForm };
|
||||||
persentasekelahiran.findMany.load();
|
persentasekelahiran.findMany.load();
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ const templateForm = z.object({
|
|||||||
name: z.string().min(3, "Judul minimal 3 karakter"),
|
name: z.string().min(3, "Judul minimal 3 karakter"),
|
||||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
imageId: z.string().nonempty(),
|
imageId: z.string().nonempty(),
|
||||||
|
whatsapp: z.string().min(10, "Whatsapp minimal 10 karakter"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
name: "",
|
name: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
|
whatsapp: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const kontakDarurat = proxy({
|
const kontakDarurat = proxy({
|
||||||
@@ -171,6 +173,7 @@ const kontakDarurat = proxy({
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
deskripsi: data.deskripsi,
|
deskripsi: data.deskripsi,
|
||||||
imageId: data.imageId,
|
imageId: data.imageId,
|
||||||
|
whatsapp: data.whatsapp,
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
return data; // Return the loaded data
|
||||||
} else {
|
} else {
|
||||||
@@ -207,6 +210,7 @@ const kontakDarurat = proxy({
|
|||||||
name: this.form.name,
|
name: this.form.name,
|
||||||
deskripsi: this.form.deskripsi,
|
deskripsi: this.form.deskripsi,
|
||||||
imageId: this.form.imageId,
|
imageId: this.form.imageId,
|
||||||
|
whatsapp: this.form.whatsapp,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,58 +5,117 @@ import { toast } from "react-toastify";
|
|||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateapbDesaForm = z.object({
|
// --- Zod Schema ---
|
||||||
name: z.string().min(1, "Judul minimal 1 karakter"),
|
const ApbdesItemSchema = z.object({
|
||||||
jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
kode: z.string().min(1, "Kode wajib diisi"),
|
||||||
imageId: z.string().min(1, "File minimal 1"),
|
uraian: z.string().min(1, "Uraian wajib diisi"),
|
||||||
fileId: z.string().min(1, "File minimal 1"),
|
anggaran: z.number().min(0),
|
||||||
|
realisasi: z.number().min(0),
|
||||||
|
selisih: z.number(),
|
||||||
|
persentase: z.number(),
|
||||||
|
level: z.number().int().min(1).max(3),
|
||||||
|
tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultapbdesForm = {
|
const ApbdesFormSchema = z.object({
|
||||||
name: "",
|
tahun: z.number().int().min(2000, "Tahun tidak valid"),
|
||||||
jumlah: "",
|
imageId: z.string().min(1, "Gambar wajib diunggah"),
|
||||||
|
fileId: z.string().min(1, "File wajib diunggah"),
|
||||||
|
items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Default Form ---
|
||||||
|
const defaultApbdesForm = {
|
||||||
|
tahun: new Date().getFullYear(),
|
||||||
imageId: "",
|
imageId: "",
|
||||||
fileId: "",
|
fileId: "",
|
||||||
|
items: [] as z.infer<typeof ApbdesItemSchema>[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
|
||||||
|
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
|
||||||
|
function normalizeItem(item: Partial<z.infer<typeof ApbdesItemSchema>>): z.infer<typeof ApbdesItemSchema> {
|
||||||
|
const anggaran = item.anggaran ?? 0;
|
||||||
|
const realisasi = item.realisasi ?? 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ✅ Formula yang benar
|
||||||
|
const selisih = anggaran - realisasi; // positif = sisa anggaran, negatif = over budget
|
||||||
|
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran
|
||||||
|
|
||||||
|
return {
|
||||||
|
kode: item.kode || "",
|
||||||
|
uraian: item.uraian || "",
|
||||||
|
anggaran,
|
||||||
|
realisasi,
|
||||||
|
selisih,
|
||||||
|
persentase,
|
||||||
|
level: item.level || 1,
|
||||||
|
tipe: item.tipe, // biarkan null jika memang null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- State Utama ---
|
||||||
const apbdes = proxy({
|
const apbdes = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: { ...defaultapbdesForm },
|
form: { ...defaultApbdesForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
|
||||||
const cek = templateapbDesaForm.safeParse(apbdes.create.form);
|
|
||||||
if (!cek.success) {
|
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}`)
|
|
||||||
.join("\n")}] required`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
apbdes.create.loading = true;
|
|
||||||
const res = await ApiFetch.api.landingpage.apbdes["create"].post({
|
|
||||||
...apbdes.create.form,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
addItem(item: Partial<z.infer<typeof ApbdesItemSchema>>) {
|
||||||
|
const normalized = normalizeItem(item);
|
||||||
|
this.form.items.push(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(index: number) {
|
||||||
|
this.form.items.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateItem(index: number, updates: Partial<z.infer<typeof ApbdesItemSchema>>) {
|
||||||
|
const current = this.form.items[index];
|
||||||
|
if (current) {
|
||||||
|
const updated = normalizeItem({ ...current, ...updates });
|
||||||
|
this.form.items[index] = updated;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.form = { ...defaultApbdesForm };
|
||||||
|
},
|
||||||
|
|
||||||
|
async create() {
|
||||||
|
const parsed = ApbdesFormSchema.safeParse(this.form);
|
||||||
|
if (!parsed.success) {
|
||||||
|
const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
|
||||||
|
toast.error(`Validasi gagal:\n${errors.join("\n")}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
const res = await ApiFetch.api.landingpage.apbdes["create"].post(parsed.data);
|
||||||
|
|
||||||
|
if (res.data?.success) {
|
||||||
|
toast.success("APBDes berhasil dibuat");
|
||||||
apbdes.findMany.load();
|
apbdes.findMany.load();
|
||||||
return toast.success("Data berhasil ditambahkan");
|
this.reset();
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message || "Gagal membuat APBDes");
|
||||||
}
|
}
|
||||||
return toast.error("Gagal menambahkan data");
|
} catch (error: any) {
|
||||||
} catch (error) {
|
console.error("Create APBDes error:", error);
|
||||||
console.log(error);
|
toast.error(error?.message || "Terjadi kesalahan saat membuat APBDes");
|
||||||
toast.error("Gagal menambahkan data");
|
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.create.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.APBDesGetPayload<{
|
| Prisma.APBDesGetPayload<{
|
||||||
include: {
|
include: { image: true; file: true; items: true };
|
||||||
image: true;
|
|
||||||
file: true;
|
|
||||||
};
|
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -64,192 +123,200 @@ const apbdes = proxy({
|
|||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
|
||||||
apbdes.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
apbdes.findMany.loading = true;
|
||||||
apbdes.findMany.page = page;
|
apbdes.findMany.page = page;
|
||||||
apbdes.findMany.search = search;
|
apbdes.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: Record<string, string> = { page: String(page), limit: String(limit) };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.apbdes[
|
const res = await ApiFetch.api.landingpage.apbdes["findMany"].get({ query });
|
||||||
"findMany"
|
|
||||||
].get({
|
|
||||||
query
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.data?.success) {
|
||||||
apbdes.findMany.data = res.data.data || [];
|
apbdes.findMany.data = res.data.data || [];
|
||||||
apbdes.findMany.total = res.data.total || 0;
|
apbdes.findMany.total = res.data.meta?.total || 0;
|
||||||
apbdes.findMany.totalPages = res.data.totalPages || 1;
|
apbdes.findMany.totalPages = res.data.meta?.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load pegawai:", res.data?.message);
|
|
||||||
apbdes.findMany.data = [];
|
apbdes.findMany.data = [];
|
||||||
apbdes.findMany.total = 0;
|
apbdes.findMany.total = 0;
|
||||||
apbdes.findMany.totalPages = 1;
|
apbdes.findMany.totalPages = 1;
|
||||||
|
toast.error(res.data?.message || "Gagal memuat data");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading pegawai:", error);
|
console.error("FindMany error:", error);
|
||||||
apbdes.findMany.data = [];
|
apbdes.findMany.data = [];
|
||||||
apbdes.findMany.total = 0;
|
apbdes.findMany.total = 0;
|
||||||
apbdes.findMany.totalPages = 1;
|
apbdes.findMany.totalPages = 1;
|
||||||
|
toast.error("Gagal memuat daftar APBDes");
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.findMany.loading = false;
|
apbdes.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.APBDesGetPayload<{
|
data: null as
|
||||||
include: {
|
| Prisma.APBDesGetPayload<{
|
||||||
image: true;
|
include: { image: true; file: true; items: true };
|
||||||
file: true;
|
}>
|
||||||
};
|
| null,
|
||||||
}> | null,
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
|
if (!id || id.trim() === '') {
|
||||||
|
this.data = null;
|
||||||
|
this.error = "ID tidak valid";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/landingpage/apbdes/${id}`);
|
// Pastikan URL-nya benar
|
||||||
if (res.ok) {
|
const url = `/api/landingpage/apbdes/${id}`;
|
||||||
const data = await res.json();
|
console.log("🌐 Fetching:", url);
|
||||||
apbdes.findUnique.data = data.data ?? null;
|
|
||||||
|
// Gunakan fetch biasa atau ApiFetch dengan cara yang benar
|
||||||
|
const response = await fetch(url);
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
console.log("📦 Response:", res);
|
||||||
|
|
||||||
|
if (res.success && res.data) {
|
||||||
|
this.data = res.data;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch data", res.status, res.statusText);
|
this.data = null;
|
||||||
apbdes.findUnique.data = null;
|
this.error = res.message || "Gagal memuat detail APBDes";
|
||||||
|
toast.error(this.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching data:", error);
|
console.error("❌ FindUnique error:", error);
|
||||||
apbdes.findUnique.data = null;
|
this.data = null;
|
||||||
|
this.error = "Gagal memuat detail APBDes";
|
||||||
|
toast.error(this.error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
|
||||||
delete: {
|
delete: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async byId(id: string) {
|
async byId(id: string) {
|
||||||
if (!id) return toast.warn("ID tidak valid");
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apbdes.delete.loading = true;
|
this.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.apbdes as any)["del"][id].delete();
|
||||||
|
|
||||||
const response = await fetch(`/api/landingpage/apbdes/del/${id}`, {
|
if (res.data?.success) {
|
||||||
method: "DELETE",
|
toast.success("APBDes berhasil dihapus");
|
||||||
headers: {
|
apbdes.findMany.load();
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
|
||||||
toast.success(result.message || "apbdes berhasil dihapus");
|
|
||||||
await apbdes.findMany.load(); // refresh list
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus apbdes");
|
toast.error(res.data?.message || "Gagal menghapus APBDes");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Gagal delete:", error);
|
console.error("Delete error:", error);
|
||||||
toast.error("Terjadi kesalahan saat menghapus apbdes");
|
toast.error(error?.message || "Terjadi kesalahan saat menghapus");
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.delete.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
edit: {
|
edit: {
|
||||||
id: "",
|
id: "",
|
||||||
form: { ...defaultapbdesForm },
|
form: { ...defaultApbdesForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
if (!id) {
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
toast.warn("ID tidak valid");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apbdes.edit.loading = true;
|
this.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.apbdes as any)[id].get();
|
||||||
|
|
||||||
const response = await fetch(`/api/landingpage/apbdes/${id}`, {
|
if (res.data?.success) {
|
||||||
method: "GET",
|
const data = res.data.data;
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
if (result?.success) {
|
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name,
|
tahun: data.tahun || new Date().getFullYear(),
|
||||||
jumlah: data.jumlah,
|
imageId: data.imageId || "",
|
||||||
imageId: data.imageId,
|
fileId: data.fileId || "",
|
||||||
fileId: data.fileId,
|
items: (data.items || []).map((item: any) => ({
|
||||||
|
kode: item.kode,
|
||||||
|
uraian: item.uraian,
|
||||||
|
anggaran: item.anggaran,
|
||||||
|
realisasi: item.realisasi,
|
||||||
|
selisih: item.selisih,
|
||||||
|
persentase: item.persentase,
|
||||||
|
level: item.level,
|
||||||
|
tipe: item.tipe || 'pendapatan',
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result?.message || "Gagal memuat data");
|
throw new Error(res.data?.message || "Gagal memuat data");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error loading apbdes:", error);
|
console.error("Edit load error:", error);
|
||||||
toast.error(
|
toast.error(error.message || "Gagal memuat data untuk diedit");
|
||||||
error instanceof Error ? error.message : "Gagal memuat data"
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.edit.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async update() {
|
async update() {
|
||||||
const cek = templateapbDesaForm.safeParse(apbdes.edit.form);
|
const parsed = ApbdesFormSchema.safeParse(this.form);
|
||||||
if (!cek.success) {
|
if (!parsed.success) {
|
||||||
const err = `[${cek.error.issues
|
const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
|
||||||
.map((v) => `${v.path.join(".")}`)
|
toast.error(`Validasi gagal:\n${errors.join("\n")}`);
|
||||||
.join("\n")}] required`;
|
return false;
|
||||||
return toast.error(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apbdes.edit.loading = true;
|
this.loading = true;
|
||||||
const response = await fetch(`/api/landingpage/apbdes/${this.id}`, {
|
// Include the ID in the request body
|
||||||
method: "PUT",
|
const requestData = {
|
||||||
headers: {
|
...parsed.data,
|
||||||
"Content-Type": "application/json",
|
id: this.id, // Add the ID to the request body
|
||||||
},
|
};
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.form.name,
|
const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData);
|
||||||
jumlah: this.form.jumlah,
|
|
||||||
imageId: this.form.imageId,
|
if (res.data?.success) {
|
||||||
fileId: this.form.fileId,
|
toast.success("APBDes berhasil diperbarui");
|
||||||
}),
|
apbdes.findMany.load();
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(
|
|
||||||
errorData.message || `HTTP error! status: ${response.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
toast.success("Berhasil update apbdes");
|
|
||||||
await apbdes.findMany.load(); // refresh list
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || "Gagal mengupdate apbdes");
|
throw new Error(res.data?.message || "Gagal memperbarui APBDes");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error updating apbdes:", error);
|
console.error("Update error:", error);
|
||||||
toast.error(
|
toast.error(error.message || "Gagal memperbarui APBDes");
|
||||||
error instanceof Error ? error.message : "Gagal mengupdate apbdes"
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.edit.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addItem(item: Partial<z.infer<typeof ApbdesItemSchema>>) {
|
||||||
|
const normalized = normalizeItem(item);
|
||||||
|
this.form.items.push(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(index: number) {
|
||||||
|
this.form.items.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
apbdes.edit.id = "";
|
this.id = "";
|
||||||
apbdes.edit.form = { ...defaultapbdesForm };
|
this.form = { ...defaultApbdesForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const programInovasi = proxy({
|
|||||||
].post(formData);
|
].post(formData);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
programInovasi.findMany.load();
|
programInovasi.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
@@ -388,16 +388,15 @@ const pejabatDesa = proxy({
|
|||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
// Ensure ID is properly encoded in the URL
|
||||||
`/api/landingpage/pejabatdesa/${this.id}`,
|
const url = new URL(`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`, window.location.origin);
|
||||||
{
|
const response = await fetch(url.toString(), {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(this.form),
|
body: JSON.stringify(this.form),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
@@ -475,7 +474,7 @@ const mediaSosial = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
mediaSosial.findMany.load();
|
mediaSosial.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -93,6 +93,34 @@ const sdgsDesa = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findManyAll: {
|
||||||
|
data: null as any[] | null,
|
||||||
|
loading: false,
|
||||||
|
load: async () => { // Change to arrow function
|
||||||
|
sdgsDesa.findManyAll.loading = true; // Use the full path to access the property
|
||||||
|
try {
|
||||||
|
const query: any = {};
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.landingpage.sdgsdesa[
|
||||||
|
"findManyAll"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
sdgsDesa.findManyAll.data = res.data.data || [];
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load media sosial:", res.data?.message);
|
||||||
|
sdgsDesa.findManyAll.data = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading media sosial:", error);
|
||||||
|
sdgsDesa.findManyAll.data = [];
|
||||||
|
} finally {
|
||||||
|
sdgsDesa.findManyAll.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.SdgsDesaGetPayload<{
|
data: null as Prisma.SdgsDesaGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const dataLingkunganDesaState = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
dataLingkunganDesaState.findMany.load();
|
dataLingkunganDesaState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const pengelolaanSampah = proxy({
|
|||||||
].post(pengelolaanSampah.create.form);
|
].post(pengelolaanSampah.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
pengelolaanSampah.findMany.load();
|
pengelolaanSampah.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const programPenghijauanState = proxy({
|
|||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
programPenghijauanState.findMany.load();
|
programPenghijauanState.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -9,34 +9,32 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const templateBeasiswaPendaftar = z.object({
|
const templateBeasiswaPendaftar = z.object({
|
||||||
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
||||||
nik: z.string().min(1, "NIK harus diisi"),
|
nis: z.string().min(1, "NIS harus diisi"),
|
||||||
|
kelas: z.string().min(1, "Kelas harus diisi"),
|
||||||
|
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
|
||||||
|
alamatDomisili: z.string().min(1, "Alamat domisili harus diisi"),
|
||||||
tempatLahir: z.string().min(1, "Tempat lahir harus diisi"),
|
tempatLahir: z.string().min(1, "Tempat lahir harus diisi"),
|
||||||
tanggalLahir: z.string().min(1, "Tanggal lahir harus diisi"),
|
tanggalLahir: z.string().min(1, "Tanggal lahir harus diisi"),
|
||||||
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
|
namaOrtu: z.string().min(1, "Nama ortu harus diisi"),
|
||||||
kewarganegaraan: z.string().min(1, "Kewarganegaraan harus diisi"),
|
nik: z.string().min(1, "NIK harus diisi"),
|
||||||
agama: z.string().min(1, "Agama harus diisi"),
|
pekerjaanOrtu: z.string().min(1, "Pekerjaan ortu harus diisi"),
|
||||||
alamatKTP: z.string().min(1, "Alamat KTP harus diisi"),
|
penghasilan: z.string().min(1, "Penghasilan ortu harus diisi"),
|
||||||
alamatDomisili: z.string().min(1, "Alamat domisili harus diisi"),
|
|
||||||
noHp: z.string().min(1, "No HP harus diisi"),
|
noHp: z.string().min(1, "No HP harus diisi"),
|
||||||
email: z.string().min(1, "Email harus diisi"),
|
|
||||||
statusPernikahan: z.string().min(1, "Status pernikahan harus diisi"),
|
|
||||||
ukuranBaju: z.string().min(1, "Ukuran baju harus diisi"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultBeasiswaPendaftar = {
|
const defaultBeasiswaPendaftar = {
|
||||||
namaLengkap: "",
|
namaLengkap: "",
|
||||||
nik: "",
|
nis: "",
|
||||||
|
kelas: "",
|
||||||
|
jenisKelamin: "",
|
||||||
|
alamatDomisili: "",
|
||||||
tempatLahir: "",
|
tempatLahir: "",
|
||||||
tanggalLahir: "",
|
tanggalLahir: "",
|
||||||
jenisKelamin: "",
|
namaOrtu: "",
|
||||||
kewarganegaraan: "",
|
nik: "",
|
||||||
agama: "",
|
pekerjaanOrtu: "",
|
||||||
alamatKTP: "",
|
penghasilan: "",
|
||||||
alamatDomisili: "",
|
|
||||||
noHp: "",
|
noHp: "",
|
||||||
email: "",
|
|
||||||
statusPernikahan: "",
|
|
||||||
ukuranBaju: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const beasiswaPendaftar = proxy({
|
const beasiswaPendaftar = proxy({
|
||||||
@@ -200,18 +198,17 @@ const beasiswaPendaftar = proxy({
|
|||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
namaLengkap: data.namaLengkap,
|
namaLengkap: data.namaLengkap,
|
||||||
nik: data.nik,
|
nis: data.nis,
|
||||||
|
kelas: data.kelas,
|
||||||
|
jenisKelamin: data.jenisKelamin,
|
||||||
|
alamatDomisili: data.alamatDomisili,
|
||||||
tempatLahir: data.tempatLahir,
|
tempatLahir: data.tempatLahir,
|
||||||
tanggalLahir: data.tanggalLahir,
|
tanggalLahir: data.tanggalLahir,
|
||||||
jenisKelamin: data.jenisKelamin,
|
namaOrtu: data.namaOrtu,
|
||||||
kewarganegaraan: data.kewarganegaraan,
|
nik: data.nik,
|
||||||
agama: data.agama,
|
pekerjaanOrtu: data.pekerjaanOrtu,
|
||||||
alamatKTP: data.alamatKTP,
|
penghasilan: data.penghasilan,
|
||||||
alamatDomisili: data.alamatDomisili,
|
|
||||||
noHp: data.noHp,
|
noHp: data.noHp,
|
||||||
email: data.email,
|
|
||||||
statusPernikahan: data.statusPernikahan,
|
|
||||||
ukuranBaju: data.ukuranBaju,
|
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
return data; // Return the loaded data
|
||||||
} else {
|
} else {
|
||||||
@@ -249,17 +246,17 @@ const beasiswaPendaftar = proxy({
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
namaLengkap: this.form.namaLengkap,
|
namaLengkap: this.form.namaLengkap,
|
||||||
nik: this.form.nik,
|
nis: this.form.nis,
|
||||||
tanggalLahir: this.form.tanggalLahir,
|
kelas: this.form.kelas,
|
||||||
jenisKelamin: this.form.jenisKelamin,
|
jenisKelamin: this.form.jenisKelamin,
|
||||||
kewarganegaraan: this.form.kewarganegaraan,
|
|
||||||
agama: this.form.agama,
|
|
||||||
alamatKTP: this.form.alamatKTP,
|
|
||||||
alamatDomisili: this.form.alamatDomisili,
|
alamatDomisili: this.form.alamatDomisili,
|
||||||
|
tempatLahir: this.form.tempatLahir,
|
||||||
|
tanggalLahir: this.form.tanggalLahir,
|
||||||
|
namaOrtu: this.form.namaOrtu,
|
||||||
|
nik: this.form.nik,
|
||||||
|
pekerjaanOrtu: this.form.pekerjaanOrtu,
|
||||||
|
penghasilan: this.form.penghasilan,
|
||||||
noHp: this.form.noHp,
|
noHp: this.form.noHp,
|
||||||
email: this.form.email,
|
|
||||||
statusPernikahan: this.form.statusPernikahan,
|
|
||||||
ukuranBaju: this.form.ukuranBaju,
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -332,7 +329,7 @@ const keunggulanProgram = proxy({
|
|||||||
].post(keunggulanProgram.create.form);
|
].post(keunggulanProgram.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
keunggulanProgram.findMany.load();
|
keunggulanProgram.findMany.load();
|
||||||
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
|
return toast.success("Data Berhasil Dibuat");
|
||||||
}
|
}
|
||||||
console.log(res);
|
console.log(res);
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const dataPendidikan = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
dataPendidikan.create.form = {
|
dataPendidikan.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
jumlah: "",
|
jumlah: "",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const dataPerpustakaan = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
|
const startTime = Date.now();
|
||||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
dataPerpustakaan.findMany.page = page;
|
dataPerpustakaan.findMany.page = page;
|
||||||
dataPerpustakaan.findMany.search = search;
|
dataPerpustakaan.findMany.search = search;
|
||||||
@@ -77,7 +78,10 @@ const dataPerpustakaan = proxy({
|
|||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
if (kategori) query.kategori = kategori;
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||||
@@ -91,7 +95,52 @@ const dataPerpustakaan = proxy({
|
|||||||
dataPerpustakaan.findMany.data = [];
|
dataPerpustakaan.findMany.data = [];
|
||||||
dataPerpustakaan.findMany.totalPages = 1;
|
dataPerpustakaan.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
|
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const minDelay = 300;
|
||||||
|
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
dataPerpustakaan.findMany.loading = false;
|
dataPerpustakaan.findMany.loading = false;
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findManyAll: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.DataPerpustakaanGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
kategori: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (search = "", kategori = "") => {
|
||||||
|
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
dataPerpustakaan.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = {};
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||||
|
"findManyAll"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
|
||||||
|
} else {
|
||||||
|
dataPerpustakaan.findManyAll.data = [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||||
|
dataPerpustakaan.findManyAll.data = [];
|
||||||
|
} finally {
|
||||||
|
dataPerpustakaan.findManyAll.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -330,7 +379,10 @@ const kategoriBuku = proxy({
|
|||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
kategoriBuku.findMany.data = res.data.data ?? [];
|
kategoriBuku.findMany.data = res.data.data ?? [];
|
||||||
@@ -514,9 +566,319 @@ const kategoriBuku = proxy({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templatePeminjamanBuku = z.object({
|
||||||
|
nama: z.string().min(1, "Nama harus diisi"),
|
||||||
|
noTelp: z.string().min(1, "No Telp harus diisi"),
|
||||||
|
alamat: z.string().min(1, "Alamat harus diisi"),
|
||||||
|
bukuId: z.string().min(1, "Buku ID harus diisi"),
|
||||||
|
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
|
||||||
|
batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
|
||||||
|
tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
|
||||||
|
catatan: z.string().min(1, "Catatan harus diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultPeminjamanBuku = {
|
||||||
|
nama: "",
|
||||||
|
noTelp: "",
|
||||||
|
alamat: "",
|
||||||
|
bukuId: "",
|
||||||
|
tanggalPinjam: "",
|
||||||
|
batasKembali: "",
|
||||||
|
tanggalKembali: "",
|
||||||
|
catatan: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FormEditData {
|
||||||
|
nama: string;
|
||||||
|
noTelp: string;
|
||||||
|
alamat: string;
|
||||||
|
bukuId: string;
|
||||||
|
buku?: {
|
||||||
|
id: string;
|
||||||
|
judul: string;
|
||||||
|
};
|
||||||
|
tanggalPinjam: string;
|
||||||
|
batasKembali: string;
|
||||||
|
tanggalKembali: string;
|
||||||
|
catatan: string;
|
||||||
|
status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
|
||||||
|
}
|
||||||
|
|
||||||
|
const editForm: FormEditData = {
|
||||||
|
nama: "",
|
||||||
|
noTelp: "",
|
||||||
|
alamat: "",
|
||||||
|
bukuId: "",
|
||||||
|
tanggalPinjam: "",
|
||||||
|
batasKembali: "",
|
||||||
|
tanggalKembali: "",
|
||||||
|
catatan: "",
|
||||||
|
status: "Dipinjam",
|
||||||
|
};
|
||||||
|
|
||||||
|
const peminjamanBuku = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...defaultPeminjamanBuku },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
peminjamanBuku.create.loading = true;
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
|
||||||
|
"create"
|
||||||
|
].post(peminjamanBuku.create.form);
|
||||||
|
if (res.status === 200) {
|
||||||
|
peminjamanBuku.findMany.load();
|
||||||
|
return toast.success("Data Peminjaman Buku Berhasil Dibuat");
|
||||||
|
}
|
||||||
|
console.log(res);
|
||||||
|
return toast.error("failed create");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return toast.error("failed create");
|
||||||
|
} finally {
|
||||||
|
peminjamanBuku.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: [] as Prisma.PeminjamanBukuGetPayload<{
|
||||||
|
include: {
|
||||||
|
buku: true;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
peminjamanBuku.findMany.page = page;
|
||||||
|
peminjamanBuku.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
peminjamanBuku.findMany.data = res.data.data ?? [];
|
||||||
|
peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
peminjamanBuku.findMany.data = [];
|
||||||
|
peminjamanBuku.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch data peminjaman buku paginated:", err);
|
||||||
|
peminjamanBuku.findMany.data = [];
|
||||||
|
peminjamanBuku.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
peminjamanBuku.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.PeminjamanBukuGetPayload<{
|
||||||
|
include: {
|
||||||
|
buku: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
peminjamanBuku.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch data", res.status, res.statusText);
|
||||||
|
peminjamanBuku.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
peminjamanBuku.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async delete(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
|
try {
|
||||||
|
peminjamanBuku.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/perpustakaandigital/peminjamanbuku/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(
|
||||||
|
result.message || "Data Peminjaman Buku berhasil dihapus"
|
||||||
|
);
|
||||||
|
await peminjamanBuku.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
result?.message || "Gagal menghapus Data Peminjaman Buku"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus Data Peminjaman Buku");
|
||||||
|
} finally {
|
||||||
|
peminjamanBuku.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...editForm },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
nama: data.nama,
|
||||||
|
noTelp: data.noTelp,
|
||||||
|
alamat: data.alamat,
|
||||||
|
bukuId: data.bukuId,
|
||||||
|
tanggalPinjam: data.tanggalPinjam,
|
||||||
|
batasKembali: data.batasKembali,
|
||||||
|
tanggalKembali: data.tanggalKembali,
|
||||||
|
catatan: data.catatan,
|
||||||
|
status: data.status,
|
||||||
|
};
|
||||||
|
return data; // Return the loaded data
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading peminjaman buku:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.update.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
peminjamanBuku.update.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
nama: this.form.nama,
|
||||||
|
noTelp: this.form.noTelp,
|
||||||
|
alamat: this.form.alamat,
|
||||||
|
bukuId: this.form.bukuId,
|
||||||
|
tanggalPinjam: this.form.tanggalPinjam,
|
||||||
|
batasKembali: this.form.batasKembali,
|
||||||
|
tanggalKembali: this.form.tanggalKembali,
|
||||||
|
catatan: this.form.catatan,
|
||||||
|
status: this.form.status,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(
|
||||||
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Berhasil update data peminjaman buku");
|
||||||
|
await peminjamanBuku.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
result.message || "Gagal update data peminjaman buku"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating data peminjaman buku:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat update data peminjaman buku"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
peminjamanBuku.update.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
peminjamanBuku.update.id = "";
|
||||||
|
peminjamanBuku.update.form = { ...editForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const perpustakaanDigitalState = proxy({
|
const perpustakaanDigitalState = proxy({
|
||||||
dataPerpustakaan,
|
dataPerpustakaan,
|
||||||
kategoriBuku,
|
kategoriBuku,
|
||||||
|
peminjamanBuku,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default perpustakaanDigitalState;
|
export default perpustakaanDigitalState;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const daftarInformasiPublik = proxy({
|
|||||||
].post(daftarInformasiPublik.create.form);
|
].post(daftarInformasiPublik.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
daftarInformasiPublik.findMany.load();
|
daftarInformasiPublik.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const grafikBerdasarkanUmur = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Sukses menambahkan");
|
||||||
grafikBerdasarkanUmur.create.form = {
|
grafikBerdasarkanUmur.create.form = {
|
||||||
remaja: "",
|
remaja: "",
|
||||||
dewasa: "",
|
dewasa: "",
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const statepermohonanInformasiPublik = proxy({
|
|||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
|
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
statepermohonanInformasiPublik.findMany.load();
|
statepermohonanInformasiPublik.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -112,7 +112,32 @@ const statepermohonanInformasiPublik = proxy({
|
|||||||
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
||||||
|
include: {
|
||||||
|
jenisInformasiDiminta: true,
|
||||||
|
caraMemperolehInformasi: true,
|
||||||
|
caraMemperolehSalinanInformasi: true,
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch program inovasi:", res.statusText);
|
||||||
|
statepermohonanInformasiPublik.findUnique.data = null;
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching program inovasi:", error);
|
||||||
|
statepermohonanInformasiPublik.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const statepermohonanInformasiPublikForm = proxy({
|
const statepermohonanInformasiPublikForm = proxy({
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const permohonanKeberatanInformasi = proxy({
|
|||||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
|
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
permohonanKeberatanInformasi.findMany.load();
|
permohonanKeberatanInformasi.findMany.load();
|
||||||
return toast.success("success create");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -57,6 +57,28 @@ const permohonanKeberatanInformasi = proxy({
|
|||||||
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
|
||||||
|
permohonanKeberatanInformasi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching permohonan keberatan informasi:", error);
|
||||||
|
permohonanKeberatanInformasi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import { toast } from "react-toastify";
|
|||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
/**
|
|
||||||
* Schema validasi form ProfilePPID menggunakan Zod.
|
|
||||||
*/
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
biodata: z.string().min(3, "Biodata minimal 3 karakter"),
|
biodata: z.string().min(3, "Biodata minimal 3 karakter"),
|
||||||
@@ -33,25 +30,16 @@ type ProfilePPIDForm = Prisma.ProfilePPIDGetPayload<{
|
|||||||
pengalaman: true;
|
pengalaman: true;
|
||||||
unggulan: true;
|
unggulan: true;
|
||||||
imageId: true;
|
imageId: true;
|
||||||
image?: {
|
image?: { select: { link: true } };
|
||||||
select: {
|
|
||||||
link: true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Improved State Management - Consolidated and more robust
|
|
||||||
*/
|
|
||||||
const stateProfilePPID = proxy({
|
const stateProfilePPID = proxy({
|
||||||
// Consolidated data management
|
|
||||||
profile: {
|
profile: {
|
||||||
data: null as ProfilePPIDForm | null,
|
data: null as ProfilePPIDForm | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null as string | null,
|
error: null as string | null,
|
||||||
|
|
||||||
// Single method to load profile data
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.warn("ID tidak valid");
|
toast.warn("ID tidak valid");
|
||||||
@@ -62,52 +50,42 @@ const stateProfilePPID = proxy({
|
|||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ppid/profileppid/${id}`);
|
const res = await fetch(`/api/ppid/profileppid/${id}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.data = result.data;
|
this.data = result.data;
|
||||||
return result.data;
|
return result.data;
|
||||||
} else {
|
} else throw new Error(result.message || "Gagal memuat data profile");
|
||||||
throw new Error(result.message || "Gagal mengambil data profile");
|
} catch (err) {
|
||||||
}
|
const msg = (err as Error).message;
|
||||||
} catch (error) {
|
this.error = msg;
|
||||||
const errorMessage = (error as Error).message;
|
console.error("Load profile error:", msg);
|
||||||
this.error = errorMessage;
|
toast.error("Gagal memuat data profile");
|
||||||
console.error("Load profile error:", errorMessage);
|
|
||||||
toast.error("Terjadi kesalahan saat mengambil data profile");
|
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Reset profile data
|
|
||||||
reset() {
|
reset() {
|
||||||
this.data = null;
|
this.data = null;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Edit form management
|
|
||||||
editForm: {
|
editForm: {
|
||||||
id: "",
|
id: "",
|
||||||
form: { ...defaultForm },
|
form: { ...defaultForm },
|
||||||
|
originalForm: { ...defaultForm }, // ✅ Tambah field originalForm
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null as string | null,
|
error: null as string | null,
|
||||||
isReadOnly: false, // Flag untuk data yang tidak bisa diedit
|
|
||||||
|
|
||||||
// Initialize form with profile data
|
|
||||||
initialize(profileData: ProfilePPIDForm) {
|
initialize(profileData: ProfilePPIDForm) {
|
||||||
this.id = profileData.id;
|
this.id = profileData.id;
|
||||||
this.isReadOnly = false; // Semua data bisa diedit
|
const data = {
|
||||||
this.form = {
|
|
||||||
name: profileData.name || "",
|
name: profileData.name || "",
|
||||||
biodata: profileData.biodata || "",
|
biodata: profileData.biodata || "",
|
||||||
riwayat: profileData.riwayat || "",
|
riwayat: profileData.riwayat || "",
|
||||||
@@ -115,23 +93,20 @@ const stateProfilePPID = proxy({
|
|||||||
unggulan: profileData.unggulan || "",
|
unggulan: profileData.unggulan || "",
|
||||||
imageId: profileData.imageId || "",
|
imageId: profileData.imageId || "",
|
||||||
};
|
};
|
||||||
|
this.form = { ...data };
|
||||||
|
this.originalForm = { ...data }; // ✅ Simpan versi original
|
||||||
},
|
},
|
||||||
|
|
||||||
// Update form field
|
|
||||||
updateField(field: keyof typeof defaultForm, value: string) {
|
updateField(field: keyof typeof defaultForm, value: string) {
|
||||||
this.form[field] = value;
|
this.form[field] = value;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Submit form
|
|
||||||
async submit() {
|
async submit() {
|
||||||
// Validate form
|
const check = templateForm.safeParse(this.form);
|
||||||
const validation = templateForm.safeParse(this.form);
|
if (!check.success) {
|
||||||
|
toast.error(
|
||||||
if (!validation.success) {
|
check.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
|
||||||
const errors = validation.error.issues
|
);
|
||||||
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
||||||
.join(", ");
|
|
||||||
toast.error(`Form tidak valid: ${errors}`);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,63 +114,54 @@ const stateProfilePPID = proxy({
|
|||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ppid/profileppid/${this.id}`, {
|
const res = await fetch(`/api/ppid/profileppid/${this.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(this.form),
|
body: JSON.stringify(this.form),
|
||||||
});
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
if (!response.ok) {
|
const result = await res.json();
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success("Berhasil update profile");
|
toast.success("Berhasil update profile");
|
||||||
// Refresh profile data
|
this.originalForm = { ...this.form }; // ✅ Update original setelah sukses
|
||||||
await stateProfilePPID.profile.load(this.id);
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else throw new Error(result.message || "Gagal update profile");
|
||||||
throw new Error(result.message || "Gagal update profile");
|
} catch (err) {
|
||||||
}
|
const msg = (err as Error).message;
|
||||||
} catch (error) {
|
this.error = msg;
|
||||||
const errorMessage = (error as Error).message;
|
toast.error(msg);
|
||||||
this.error = errorMessage;
|
|
||||||
console.error("Update profile error:", errorMessage);
|
|
||||||
toast.error("Terjadi kesalahan saat update profile");
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Reset form
|
// ✅ Tambahan reset ke original data
|
||||||
|
resetToOriginal() {
|
||||||
|
this.form = { ...this.originalForm };
|
||||||
|
toast.info("Data dikembalikan ke kondisi awal");
|
||||||
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.id = "";
|
this.id = "";
|
||||||
this.form = { ...defaultForm };
|
this.form = { ...defaultForm };
|
||||||
|
this.originalForm = { ...defaultForm };
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.isReadOnly = false;
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
async loadForEdit(id: string) {
|
async loadForEdit(id: string) {
|
||||||
const profileData = await this.profile.load(id);
|
const data = await this.profile.load(id);
|
||||||
if (profileData) {
|
if (data) this.editForm.initialize(data);
|
||||||
this.editForm.initialize(profileData);
|
return data;
|
||||||
}
|
|
||||||
return profileData;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.profile.reset();
|
this.profile.reset();
|
||||||
this.editForm.reset();
|
this.editForm.reset();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default stateProfilePPID;
|
export default stateProfilePPID;
|
||||||
@@ -381,7 +381,44 @@ const posisiOrganisasi = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findManyAll: {
|
||||||
|
data: [] as Array<{
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
deskripsi: string | null;
|
||||||
|
hierarki: number;
|
||||||
|
}>,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
|
||||||
|
posisiOrganisasi.findManyAll.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { search };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
|
||||||
|
"find-many-all"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
posisiOrganisasi.findManyAll.data = res.data.data || [];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load posisiOrganisasi:", res.data?.message);
|
||||||
|
posisiOrganisasi.findManyAll.data = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading posisiOrganisasi:", error);
|
||||||
|
posisiOrganisasi.findManyAll.data = [];
|
||||||
|
} finally {
|
||||||
|
posisiOrganisasi.findManyAll.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
delete: {
|
delete: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async byId(id: string) {
|
async byId(id: string) {
|
||||||
@@ -524,9 +561,48 @@ const pegawai = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findManyAll: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.PegawaiPPIDGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
posisi: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
pegawai.findManyAll.loading = true; // Use the full path to access the property
|
||||||
|
pegawai.findManyAll.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { search };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||||
|
"find-many-all"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
pegawai.findManyAll.data = res.data.data || [];
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load pegawai:", res.data?.message);
|
||||||
|
pegawai.findManyAll.data = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading pegawai:", error);
|
||||||
|
pegawai.findManyAll.data = [];
|
||||||
|
} finally {
|
||||||
|
pegawai.findManyAll.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as
|
data: null as
|
||||||
| (Prisma.PegawaiGetPayload<{
|
| (Prisma.PegawaiPPIDGetPayload<{
|
||||||
include: { posisi: true; image: true };
|
include: { posisi: true; image: true };
|
||||||
}> & { isActive: boolean })
|
}> & { isActive: boolean })
|
||||||
| null,
|
| null,
|
||||||
@@ -571,6 +647,31 @@ const pegawai = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
nonActive: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
try {
|
||||||
|
pegawai.nonActive.loading = true;
|
||||||
|
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
|
||||||
|
method: "DELETE", // biasanya nonActive pakai PATCH
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
|
||||||
|
await pegawai.findMany.load(); // refresh data
|
||||||
|
} else {
|
||||||
|
toast.error(json.message ?? "Gagal menonaktifkan pegawai");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal nonActive:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
|
||||||
|
} finally {
|
||||||
|
pegawai.nonActive.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
edit: {
|
edit: {
|
||||||
id: "",
|
id: "",
|
||||||
form: { ...pegawaiDefaultForm },
|
form: { ...pegawaiDefaultForm },
|
||||||
|
|||||||
@@ -90,27 +90,83 @@ const userState = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updateActive: {
|
deleteUser: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async submit(id: string, isActive: boolean) {
|
|
||||||
|
async delete(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
|
try {
|
||||||
|
userState.deleteUser.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(`/api/user/delUser/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(result.message || "User berhasil dihapus permanen");
|
||||||
|
await userState.findMany.load(); // refresh list user setelah delete
|
||||||
|
} else {
|
||||||
|
toast.error(result?.message || "Gagal menghapus user");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete user:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus user");
|
||||||
|
} finally {
|
||||||
|
userState.deleteUser.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Di file userState.ts atau dimana state user berada
|
||||||
|
|
||||||
|
update: {
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/updt`, {
|
const res = await fetch(`/api/user/updt`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ id, isActive }),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.status === 200 && data.success) {
|
if (res.status === 200 && data.success) {
|
||||||
toast.success(data.message);
|
// ✅ Tampilkan pesan yang berbeda jika role berubah
|
||||||
userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
|
if (data.roleChanged) {
|
||||||
|
toast.success(
|
||||||
|
`${data.message}\n\nUser akan logout otomatis dalam beberapa detik.`,
|
||||||
|
{
|
||||||
|
autoClose: 5000,
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || "Gagal update status user");
|
toast.success(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh list
|
||||||
|
await userState.findMany.load(
|
||||||
|
userState.findMany.page,
|
||||||
|
10,
|
||||||
|
userState.findMany.search
|
||||||
|
);
|
||||||
|
|
||||||
|
return true; // ✅ Return success untuk handling di component
|
||||||
|
} else {
|
||||||
|
toast.error(data.message || "Gagal update user");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error("❌ Error update user:", e);
|
||||||
toast.error("Gagal update status user");
|
toast.error("Gagal update user");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -120,12 +176,10 @@ const userState = proxy({
|
|||||||
|
|
||||||
const templateRole = z.object({
|
const templateRole = z.object({
|
||||||
name: z.string().min(1, "Nama harus diisi"),
|
name: z.string().min(1, "Nama harus diisi"),
|
||||||
permissions: z.array(z.string()).min(1, "Permission harus diisi"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultRole = {
|
const defaultRole = {
|
||||||
name: "",
|
name: "",
|
||||||
permissions: [] as string[],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleState = proxy({
|
const roleState = proxy({
|
||||||
@@ -245,29 +299,23 @@ const roleState = proxy({
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
this.id = data.id;
|
|
||||||
this.form = {
|
// langsung set melalui root state, bukan this
|
||||||
|
roleState.update.id = data.id;
|
||||||
|
roleState.update.form = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
permissions: data.permissions,
|
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
|
||||||
} else {
|
return data;
|
||||||
throw new Error(result?.message || "Gagal memuat data");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading role:", error);
|
console.error("Error loading role:", error);
|
||||||
toast.error(
|
toast.error("Gagal memuat data");
|
||||||
error instanceof Error ? error.message : "Gagal memuat data"
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async update() {
|
async update() {
|
||||||
@@ -290,7 +338,6 @@ const roleState = proxy({
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: this.form.name,
|
name: this.form.name,
|
||||||
permissions: this.form.permissions,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,103 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
|
import { apiFetchLogin } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PhoneInput } from "react-international-phone";
|
import { PhoneInput } from 'react-international-phone';
|
||||||
import "react-international-phone/style.css";
|
import 'react-international-phone/style.css';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const [phone, setPhone] = useState("")
|
const [phone, setPhone] = useState('');
|
||||||
const [isError, setError] = useState(false)
|
const [loading, setLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
|
// Login.tsx
|
||||||
async function onLogin() {
|
async function onLogin() {
|
||||||
const nomor = phone.substring(1);
|
|
||||||
if (nomor.length <= 4) return setError(true)
|
|
||||||
|
|
||||||
|
const cleanPhone = phone.replace(/\D/g, '');
|
||||||
|
console.log(cleanPhone);
|
||||||
|
if (cleanPhone.length < 10) {
|
||||||
|
toast.error('Nomor telepon tidak valid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await apiFetchLogin({ nomor: nomor })
|
const response = await apiFetchLogin({ nomor: cleanPhone });
|
||||||
if (response && response.success) {
|
|
||||||
localStorage.setItem("hipmi_auth_code_id", response.kodeId);
|
console.log(response);
|
||||||
toast.success(response.message);
|
|
||||||
router.push("/validasi", { scroll: false });
|
if (!response.success) {
|
||||||
|
toast.error(response.message || 'Gagal memproses login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan nomor untuk register
|
||||||
|
localStorage.setItem('auth_nomor', cleanPhone);
|
||||||
|
if (response.isRegistered) {
|
||||||
|
// ✅ User lama: simpan kodeId
|
||||||
|
localStorage.setItem('auth_kodeId', response.kodeId);
|
||||||
|
|
||||||
|
// ✅ Cookie sudah di-set oleh API, langsung redirect
|
||||||
|
router.push('/validasi'); // Clean URL
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
// ❌ User baru: langsung ke registrasi (tanpa kodeId)
|
||||||
toast.error(response?.message);
|
router.push('/registrasi');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false)
|
console.error('Error Login:', error);
|
||||||
console.log("Error Login", error)
|
toast.error('Terjadi kesalahan saat login');
|
||||||
toast.error("Terjadi kesalahan saat login")
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg}>
|
<Stack pos="relative" bg={colors.Bg}>
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||||
<Stack align='center' justify='center' h={"100vh"}>
|
<Stack align="center" justify="center" h="100vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center' gap={"lg"}>
|
<Stack align="center" gap="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title ta="center" order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Login
|
Login
|
||||||
</Title>
|
</Title>
|
||||||
<Center>
|
<Center>
|
||||||
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
|
<Image
|
||||||
|
loading="lazy"
|
||||||
|
src="/darmasaba-icon.png"
|
||||||
|
alt="Logo"
|
||||||
|
w={80}
|
||||||
|
h={80}
|
||||||
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
{/* <Box mb={10}>
|
|
||||||
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masuk Untuk Akses Admin</Text>
|
|
||||||
<TextInput
|
|
||||||
label='Username'
|
|
||||||
placeholder='Username'
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Box> */}
|
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
countrySelectorStyleProps={{
|
countrySelectorStyleProps={{
|
||||||
buttonStyle: {
|
buttonStyle: {
|
||||||
backgroundColor: colors['blue-button'],
|
backgroundColor: colors['blue-button'],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
inputStyle={{ width: "100%"}}
|
inputStyle={{ width: '100%' }}
|
||||||
defaultCountry="id"
|
defaultCountry="id"
|
||||||
onChange={(val) => {
|
value={phone}
|
||||||
setPhone(val);
|
onChange={(val) => setPhone(val)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isError ? (
|
|
||||||
toast.error("Masukan nomor telepon anda")
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<Box py={20}>
|
<Box py={20}>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
radius={'xl'}
|
radius="xl"
|
||||||
onClick={onLogin}
|
onClick={onLogin}
|
||||||
loading={loading ? true : false}
|
loading={loading}
|
||||||
>Masuk
|
>
|
||||||
|
Masuk
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex justify={'center'} align={'center'}>
|
|
||||||
<Text>Belum punya akun? </Text>
|
|
||||||
<Button variant='transparent' component={Link} href={'/registrasi'}>
|
|
||||||
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,113 +1,127 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
// app/registrasi/page.tsx
|
||||||
'use client'
|
'use client';
|
||||||
import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
|
|
||||||
|
import { apiFetchRegister } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { PhoneInput } from "react-international-phone";
|
import { PhoneInput } from 'react-international-phone';
|
||||||
import "react-international-phone/style.css";
|
import 'react-international-phone/style.css';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
function Registrasi() {
|
export default function Registrasi() {
|
||||||
const [phone, setPhone] = useState("")
|
const router = useRouter();
|
||||||
const router = useRouter()
|
const [username, setUsername] = useState('');
|
||||||
const [value, setValue] = useState("")
|
|
||||||
const [isValue, setIsValue] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
||||||
|
|
||||||
async function onRegistarsi() {
|
// Ambil data dari localStorage (dari login)
|
||||||
if (value.length < 5) {
|
useEffect(() => {
|
||||||
toast.error("Username minimal 5 karakter!");
|
const storedNomor = localStorage.getItem('auth_nomor');
|
||||||
|
if (!storedNomor) {
|
||||||
|
toast.error('Akses tidak valid');
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPhone(storedNomor);
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!username || username.trim().length < 5) {
|
||||||
|
toast.error('Username minimal 5 karakter!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (username.includes(' ')) {
|
||||||
|
toast.error('Username tidak boleh ada spasi!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.includes(" ")) {
|
const cleanPhone = phone.replace(/\D/g, '');
|
||||||
toast.error("Username tidak boleh ada spasi!");
|
if (cleanPhone.length < 10) {
|
||||||
return;
|
toast.error('Nomor tidak valid!');
|
||||||
}
|
|
||||||
|
|
||||||
if (!phone) {
|
|
||||||
toast.error("Nomor telepon wajib diisi!");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const respone = await apiFetchRegister({ nomor: phone, username: value });
|
// ✅ Hanya kirim username & nomor → dapat kodeId
|
||||||
|
const response = await apiFetchRegister({ username, nomor: cleanPhone });
|
||||||
|
|
||||||
if (respone.success) {
|
if (response.success) {
|
||||||
router.push("/login", { scroll: false });
|
// Simpan sementara
|
||||||
toast.success(respone.message);
|
localStorage.setItem('auth_kodeId', response.kodeId);
|
||||||
|
localStorage.setItem('auth_username', username); // simpan username
|
||||||
|
|
||||||
} else {
|
toast.success('Kode verifikasi dikirim!');
|
||||||
setLoading(false);
|
router.push('/validasi'); // ✅ ke halaman validasi
|
||||||
toast.error(respone.message);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error Registrasi:', error);
|
||||||
|
toast.error('Gagal mengirim OTP');
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
console.log("Error Registrasi", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} gap={"22"} py={"xl"} h={"100vh"}>
|
<Stack pos="relative" bg={colors.Bg} gap="22" py="xl" h="100vh">
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Stack justify='center' align='center' h={"80vh"}>
|
<Stack justify="center" align="center" h="80vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center'>
|
<Stack align="center">
|
||||||
<Title order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Registrasi
|
Registrasi
|
||||||
</Title>
|
</Title>
|
||||||
<Center>
|
<Center>
|
||||||
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
|
<Image loading="lazy" src="/darmasaba-icon.png" alt="" w={80} />
|
||||||
</Center>
|
</Center>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
<TextInput placeholder='Username'
|
<TextInput
|
||||||
label='Username'
|
label="Username"
|
||||||
maxLength={50}
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.currentTarget.value)}
|
||||||
error={
|
error={
|
||||||
value.length > 0 && value.length < 5
|
username.length > 0 && username.length < 5
|
||||||
? "Minimal 5 karakter !"
|
? 'Minimal 5 karakter!'
|
||||||
: value.includes(" ")
|
: username.includes(' ')
|
||||||
? "Tidak boleh ada spasi"
|
? 'Tidak boleh ada spasi'
|
||||||
: isValue
|
: ''
|
||||||
? "Masukan username anda"
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
onChange={(val) => {
|
|
||||||
val.currentTarget.value.length > 0 ? setIsValue(false) : "";
|
|
||||||
setValue(val.currentTarget.value);
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
/>
|
<Box pt="md">
|
||||||
<Box py={10}>
|
<Text fz="sm">Nomor Telepon</Text>
|
||||||
<Text fz={"sm"} >Nomor Telepon</Text>
|
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
countrySelectorStyleProps={{
|
|
||||||
buttonStyle: {
|
|
||||||
backgroundColor: colors['blue-button'],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
inputStyle={{ width: "100%" }}
|
|
||||||
defaultCountry="id"
|
defaultCountry="id"
|
||||||
onChange={(val) => {
|
value={phone}
|
||||||
setPhone(val);
|
disabled
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box pb={10}>
|
|
||||||
<Checkbox
|
<Box pt="md">
|
||||||
label="Saya menyetujui syarat dan ketentuan yang berlaku"
|
<Checkbox label="Saya menyetujui syarat dan ketentuan" defaultChecked />
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box pb={20} >
|
|
||||||
<Button fullWidth bg={colors['blue-button']} radius={'xl'} onClick={onRegistarsi} loading={loading ? true : false}>Daftar</Button>
|
<Box pt="xl">
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
radius="xl"
|
||||||
|
onClick={handleRegister}
|
||||||
|
loading={loading}
|
||||||
|
disabled={username.length < 5}
|
||||||
|
>
|
||||||
|
Kirim Kode Verifikasi
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -117,5 +131,3 @@ function Registrasi() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Registrasi;
|
|
||||||
|
|||||||
@@ -1,31 +1,306 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
function Validasi() {
|
import colors from '@/con/colors';
|
||||||
const router = useRouter()
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
PinInput,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { authStore } from '@/store/authStore';
|
||||||
|
|
||||||
|
export default function Validasi() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [nomor, setNomor] = useState<string | null>(null);
|
||||||
|
const [otp, setOtp] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [kodeId, setKodeId] = useState<string | null>(null);
|
||||||
|
const [isRegistrationFlow, setIsRegistrationFlow] = useState(false);
|
||||||
|
|
||||||
|
// ✅ Deteksi flow dari cookie via API
|
||||||
|
useEffect(() => {
|
||||||
|
const checkFlow = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/get-flow', {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setIsRegistrationFlow(data.flow === 'register');
|
||||||
|
console.log('🔍 Flow detected from cookie:', data.flow);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error getting flow:', error);
|
||||||
|
setIsRegistrationFlow(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkFlow();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedKodeId = localStorage.getItem('auth_kodeId');
|
||||||
|
if (!storedKodeId) {
|
||||||
|
toast.error('Akses tidak valid');
|
||||||
|
router.replace('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setKodeId(storedKodeId);
|
||||||
|
const loadOtpData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (res.ok && result.data?.nomor) {
|
||||||
|
setNomor(result.data.nomor);
|
||||||
|
} else {
|
||||||
|
throw new Error('Data OTP tidak valid');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal memuat data OTP:', error);
|
||||||
|
toast.error('Kode verifikasi tidak valid');
|
||||||
|
router.replace('/login');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadOtpData();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
if (!kodeId || !nomor || otp.length < 4) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (isRegistrationFlow) {
|
||||||
|
await handleRegistrationVerification();
|
||||||
|
} else {
|
||||||
|
await handleLoginVerification();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saat verifikasi:', error);
|
||||||
|
toast.error('Terjadi kesalahan sistem');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegistrationVerification = async () => {
|
||||||
|
const username = localStorage.getItem('auth_username');
|
||||||
|
if (!username) {
|
||||||
|
toast.error('Data registrasi tidak ditemukan.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanNomor = nomor?.replace(/\D/g, '') ?? '';
|
||||||
|
if (cleanNomor.length < 10 || username.trim().length < 5) {
|
||||||
|
toast.error('Data tidak valid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Verify OTP
|
||||||
|
const verifyRes = await fetch('/api/auth/verify-otp-register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
const verifyData = await verifyRes.json();
|
||||||
|
if (!verifyRes.ok) {
|
||||||
|
toast.error(verifyData.message || 'Verifikasi OTP gagal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Finalize registration
|
||||||
|
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor: cleanNomor, username, kodeId }),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await finalizeRes.json();
|
||||||
|
|
||||||
|
// ✅ Check JSON response (bukan redirect)
|
||||||
|
if (data.success) {
|
||||||
|
toast.success('Registrasi berhasil! Menunggu persetujuan admin.');
|
||||||
|
await cleanupStorage();
|
||||||
|
|
||||||
|
// ✅ Client-side redirect
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/waiting-room';
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
toast.error(data.message || 'Registrasi gagal');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginVerification = async () => {
|
||||||
|
const loginRes = await fetch('/api/auth/verify-otp-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginData = await loginRes.json();
|
||||||
|
|
||||||
|
if (!loginRes.ok) {
|
||||||
|
toast.error(loginData.message || 'Verifikasi gagal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, name, roleId, isActive } = loginData.user;
|
||||||
|
|
||||||
|
authStore.setUser({
|
||||||
|
id,
|
||||||
|
name: name || 'User',
|
||||||
|
roleId: Number(roleId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Cleanup setelah login sukses
|
||||||
|
await cleanupStorage();
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
window.location.href = '/waiting-room';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectPath = getRedirectPath(Number(roleId));
|
||||||
|
router.replace(redirectPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRedirectPath = (roleId: number): string => {
|
||||||
|
switch (roleId) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
return '/admin/landing-page/profil/program-inovasi';
|
||||||
|
case 3:
|
||||||
|
return '/admin/kesehatan/posyandu';
|
||||||
|
case 4:
|
||||||
|
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
|
default:
|
||||||
|
return '/admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ CLEANUP FUNCTION - Hapus localStorage + Cookie
|
||||||
|
const cleanupStorage = async () => {
|
||||||
|
// Clear localStorage
|
||||||
|
localStorage.removeItem('auth_kodeId');
|
||||||
|
localStorage.removeItem('auth_nomor');
|
||||||
|
localStorage.removeItem('auth_username');
|
||||||
|
|
||||||
|
// Clear cookie
|
||||||
|
try {
|
||||||
|
await fetch('/api/auth/clear-flow', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing flow cookie:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResend = async () => {
|
||||||
|
if (!nomor) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/resend', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
localStorage.setItem('auth_kodeId', data.kodeId);
|
||||||
|
toast.success('OTP baru dikirim');
|
||||||
|
} else {
|
||||||
|
toast.error(data.message || 'Gagal mengirim ulang OTP');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal menghubungi server');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg}>
|
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
|
||||||
|
<Loader size="md" color={colors['blue-button']} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nomor) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack pos="relative" bg={colors.Bg}>
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||||
<Stack align='center' justify='center' h={"100vh"}>
|
<Stack align="center" justify="center" h="100vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center' gap={"lg"}>
|
<Stack align="center" gap="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title ta="center" order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Kode Verifikasi
|
{isRegistrationFlow ? 'Verifikasi Registrasi' : 'Verifikasi Login'}
|
||||||
</Title>
|
</Title>
|
||||||
|
<Text ta="center" size="sm" c="dimmed" mt="xs">
|
||||||
|
Kami telah mengirim kode ke nomor <strong>{nomor}</strong>
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
<Box mb={10}>
|
<Box mb={20}>
|
||||||
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masukkan Kode Verifikasi</Text>
|
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
||||||
<PinInput type={/^[0-9]*$/} inputType="tel" inputMode="numeric" />
|
Masukkan Kode Verifikasi
|
||||||
|
</Text>
|
||||||
|
<Center>
|
||||||
|
<PinInput
|
||||||
|
length={4}
|
||||||
|
value={otp}
|
||||||
|
onChange={setOtp}
|
||||||
|
onComplete={handleVerify}
|
||||||
|
inputMode="numeric"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
<Box py={20} >
|
|
||||||
<Button onClick={() => router.push("/admin/landing-page/profile/program-inovasi")}>
|
<Button
|
||||||
Page
|
fullWidth
|
||||||
|
onClick={handleVerify}
|
||||||
|
loading={loading}
|
||||||
|
disabled={otp.length < 4}
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
Verifikasi
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
|
||||||
|
<Text ta="center" size="sm" mt="md">
|
||||||
|
Tidak menerima kode?{' '}
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={handleResend}
|
||||||
|
size="xs"
|
||||||
|
p={0}
|
||||||
|
h="auto"
|
||||||
|
color={colors['blue-button']}
|
||||||
|
>
|
||||||
|
Kirim Ulang
|
||||||
|
</Button>
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -34,5 +309,3 @@ function Validasi() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Validasi;
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { IconBuildingStore, IconFileText, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -14,36 +14,31 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
label: "Pelayanan Surat Keterangan",
|
label: "Pelayanan Surat Keterangan",
|
||||||
value: "pelayanansuratketerangan",
|
value: "pelayanansuratketerangan",
|
||||||
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
|
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
|
||||||
icon: <IconFileText size={18} stroke={1.8} />,
|
icon: <IconFileText size={18} stroke={1.8} />
|
||||||
tooltip: "Layanan terkait surat keterangan resmi desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Perizinan Berusaha",
|
label: "Pelayanan Perizinan Berusaha",
|
||||||
value: "pelayananperizinanusaha",
|
value: "pelayananperizinanusaha",
|
||||||
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
|
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
|
||||||
icon: <IconBuildingStore size={18} stroke={1.8} />,
|
icon: <IconBuildingStore size={18} stroke={1.8} />
|
||||||
tooltip: "Layanan untuk izin usaha masyarakat"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Telunjuk Sakti Desa",
|
label: "Pelayanan Telunjuk Sakti Desa",
|
||||||
value: "pelayanantelunjuksaktidesa",
|
value: "pelayanantelunjuksaktidesa",
|
||||||
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
|
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
|
||||||
icon: <IconSparkles size={18} stroke={1.8} />,
|
icon: <IconSparkles size={18} stroke={1.8} />
|
||||||
tooltip: "Layanan inovasi khusus desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Penduduk Non-Permanent",
|
label: "Pelayanan Penduduk Non-Permanent",
|
||||||
value: "pelayanannonpermanent",
|
value: "pelayanannonpermanent",
|
||||||
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
|
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
|
||||||
icon: <IconUsers size={18} stroke={1.8} />,
|
icon: <IconUsers size={18} stroke={1.8} />
|
||||||
tooltip: "Pendataan penduduk non-permanent"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Ajukan Permohonan",
|
label: "Ajukan Permohonan",
|
||||||
value: "ajukanpermohonan",
|
value: "ajukanpermohonan",
|
||||||
href: "/admin/desa/layanan/ajukan_permohonan",
|
href: "/admin/desa/layanan/ajukan_permohonan",
|
||||||
icon: <IconUsersPlus size={18} stroke={1.8} />,
|
icon: <IconUsersPlus size={18} stroke={1.8} />
|
||||||
tooltip: "Ajukan permohonan"
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -91,14 +86,8 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<Tooltip
|
|
||||||
key={i}
|
|
||||||
label={tab.tooltip}
|
|
||||||
position="bottom"
|
|
||||||
withArrow
|
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
|
||||||
>
|
|
||||||
<TabsTab
|
<TabsTab
|
||||||
|
key={i}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
leftSection={tab.icon}
|
leftSection={tab.icon}
|
||||||
style={{
|
style={{
|
||||||
@@ -109,7 +98,6 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTab>
|
</TabsTab>
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { IconCategory, IconNews } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { IconNews, IconCategory } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -15,15 +15,13 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
|||||||
label: "List Berita",
|
label: "List Berita",
|
||||||
value: "list_berita",
|
value: "list_berita",
|
||||||
href: "/admin/desa/berita/list-berita",
|
href: "/admin/desa/berita/list-berita",
|
||||||
icon: <IconNews size={18} stroke={1.8} />,
|
icon: <IconNews size={18} stroke={1.8} />
|
||||||
tooltip: "Lihat dan kelola semua berita desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Berita",
|
label: "Kategori Berita",
|
||||||
value: "kategori_berita",
|
value: "kategori_berita",
|
||||||
href: "/admin/desa/berita/kategori-berita",
|
href: "/admin/desa/berita/kategori-berita",
|
||||||
icon: <IconCategory size={18} stroke={1.8} />,
|
icon: <IconCategory size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola kategori berita desa"
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -71,14 +69,8 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<Tooltip
|
|
||||||
key={i}
|
|
||||||
label={tab.tooltip}
|
|
||||||
position="bottom"
|
|
||||||
withArrow
|
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
|
||||||
>
|
|
||||||
<TabsTab
|
<TabsTab
|
||||||
|
key={i}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
leftSection={tab.icon}
|
leftSection={tab.icon}
|
||||||
style={{
|
style={{
|
||||||
@@ -89,7 +81,6 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTab>
|
</TabsTab>
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +11,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip
|
Loader
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -22,9 +23,14 @@ function EditKategoriBerita() {
|
|||||||
const editState = useProxy(stateDashboardBerita.kategoriBerita);
|
const editState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: editState.update.form.name || '',
|
name: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,6 +44,9 @@ function EditKategoriBerita() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
});
|
});
|
||||||
|
setOriginalData({
|
||||||
|
name: data.name || '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading kategori Berita:', error);
|
console.error('Error loading kategori Berita:', error);
|
||||||
@@ -48,8 +57,24 @@ function EditKategoriBerita() {
|
|||||||
loadKategori();
|
loadKategori();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
name: originalData.name,
|
||||||
|
});
|
||||||
|
toast.info('Form dikembalikan ke data awal');
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
// update global state hanya saat submit
|
||||||
editState.update.form = {
|
editState.update.form = {
|
||||||
...editState.update.form,
|
...editState.update.form,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
@@ -61,6 +86,8 @@ function EditKategoriBerita() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating kategori Berita:', error);
|
console.error('Error updating kategori Berita:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
|
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,7 +95,6 @@ function EditKategoriBerita() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Back Button + Title */}
|
{/* Back Button + Title */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -77,7 +103,6 @@ function EditKategoriBerita() {
|
|||||||
>
|
>
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Kategori Berita
|
Edit Kategori Berita
|
||||||
</Title>
|
</Title>
|
||||||
@@ -94,14 +119,26 @@ function EditKategoriBerita() {
|
|||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
name="name"
|
||||||
label="Nama Kategori Berita"
|
label="Nama Kategori Berita"
|
||||||
placeholder="Masukkan nama kategori berita"
|
placeholder="Masukkan nama kategori berita"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={handleChange}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -112,7 +149,7 @@ function EditKategoriBerita() {
|
|||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -7,18 +7,20 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Loader
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function CreateKategoriBerita() {
|
function CreateKategoriBerita() {
|
||||||
const createState = useProxy(stateDashboardBerita.kategoriBerita);
|
const createState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
@@ -27,16 +29,23 @@ function CreateKategoriBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
await createState.create.create();
|
await createState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/desa/berita/kategori-berita');
|
router.push('/admin/desa/berita/kategori-berita');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating kategori berita:', error);
|
||||||
|
toast.error('Gagal menambahkan kategori berita');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header dengan back button */}
|
{/* Header dengan back button */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -45,7 +54,6 @@ function CreateKategoriBerita() {
|
|||||||
>
|
>
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Kategori Berita
|
Tambah Kategori Berita
|
||||||
</Title>
|
</Title>
|
||||||
@@ -62,7 +70,7 @@ function CreateKategoriBerita() {
|
|||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>}
|
label="Nama Kategori Berita"
|
||||||
placeholder="Masukkan nama kategori berita"
|
placeholder="Masukkan nama kategori berita"
|
||||||
value={createState.create.form.name || ''}
|
value={createState.create.form.name || ''}
|
||||||
onChange={(e) => (createState.create.form.name = e.target.value)}
|
onChange={(e) => (createState.create.form.name = e.target.value)}
|
||||||
@@ -70,6 +78,17 @@ function CreateKategoriBerita() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -80,7 +99,7 @@ function CreateKategoriBerita() {
|
|||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -86,7 +85,6 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>Daftar Kategori Berita</Title>
|
<Title order={4}>Daftar Kategori Berita</Title>
|
||||||
<Tooltip label="Tambah Kategori Berita" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -97,7 +95,6 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
@@ -123,7 +120,6 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Tooltip label="Edit Kategori Berita" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
@@ -135,10 +131,8 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
<IconEdit size={18} />
|
<IconEdit size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Tooltip label="Hapus Kategori Berita" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -150,7 +144,6 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
<IconTrash size={18} />
|
<IconTrash size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
|
|||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
@@ -16,10 +17,15 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Loader
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Dropzone } from "@mantine/dropzone";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
import {
|
||||||
|
IconArrowBack,
|
||||||
|
IconPhoto,
|
||||||
|
IconUpload,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -33,16 +39,28 @@ function EditBerita() {
|
|||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: beritaState.berita.edit.form.judul || "",
|
judul: "",
|
||||||
deskripsi: beritaState.berita.edit.form.deskripsi || "",
|
deskripsi: "",
|
||||||
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
|
kategoriBeritaId: "",
|
||||||
content: beritaState.berita.edit.form.content || "",
|
content: "",
|
||||||
imageId: beritaState.berita.edit.form.imageId || "",
|
imageId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load berita by id saat pertama kali
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
judul: "",
|
||||||
|
deskripsi: "",
|
||||||
|
kategoriBeritaId: "",
|
||||||
|
content: "",
|
||||||
|
imageId: "",
|
||||||
|
imageUrl: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load kategori + berita
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
beritaState.kategoriBerita.findMany.load();
|
beritaState.kategoriBerita.findMany.load();
|
||||||
|
|
||||||
const loadBerita = async () => {
|
const loadBerita = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -58,6 +76,15 @@ function EditBerita() {
|
|||||||
imageId: data.imageId || "",
|
imageId: data.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setOriginalData({
|
||||||
|
judul: data.judul || "",
|
||||||
|
deskripsi: data.deskripsi || "",
|
||||||
|
kategoriBeritaId: data.kategoriBeritaId || "",
|
||||||
|
content: data.content || "",
|
||||||
|
imageId: data.imageId || "",
|
||||||
|
imageUrl: data.image?.link || ""
|
||||||
|
});
|
||||||
|
|
||||||
if (data?.image?.link) {
|
if (data?.image?.link) {
|
||||||
setPreviewImage(data.image.link);
|
setPreviewImage(data.image.link);
|
||||||
}
|
}
|
||||||
@@ -71,8 +98,14 @@ function EditBerita() {
|
|||||||
loadBerita();
|
loadBerita();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
// Update global state hanya sekali di sini
|
||||||
beritaState.berita.edit.form = {
|
beritaState.berita.edit.form = {
|
||||||
...beritaState.berita.edit.form,
|
...beritaState.berita.edit.form,
|
||||||
...formData,
|
...formData,
|
||||||
@@ -98,13 +131,28 @@ function EditBerita() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating berita:", error);
|
console.error("Error updating berita:", error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui berita");
|
toast.error("Terjadi kesalahan saat memperbarui berita");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
judul: originalData.judul,
|
||||||
|
deskripsi: originalData.deskripsi,
|
||||||
|
kategoriBeritaId: originalData.kategoriBeritaId,
|
||||||
|
content: originalData.content,
|
||||||
|
imageId: originalData.imageId,
|
||||||
|
});
|
||||||
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
|
setFile(null);
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -113,12 +161,12 @@ function EditBerita() {
|
|||||||
>
|
>
|
||||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Berita
|
Edit Berita
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: "100%", md: "50%" }}
|
w={{ base: "100%", md: "50%" }}
|
||||||
bg={colors["white-1"]}
|
bg={colors["white-1"]}
|
||||||
@@ -132,96 +180,13 @@ function EditBerita() {
|
|||||||
label="Judul"
|
label="Judul"
|
||||||
placeholder="Masukkan judul"
|
placeholder="Masukkan judul"
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleChange("judul", e.target.value)}
|
||||||
setFormData({ ...formData, judul: e.target.value })
|
|
||||||
}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
|
||||||
label="Deskripsi"
|
|
||||||
placeholder="Masukkan deskripsi"
|
|
||||||
value={formData.deskripsi}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, deskripsi: e.target.value })
|
|
||||||
}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
|
||||||
Gambar Berita
|
|
||||||
</Text>
|
|
||||||
<Dropzone
|
|
||||||
onDrop={(files) => {
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={180}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={48} color="red" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
<Stack gap="xs" align="center">
|
|
||||||
<Text size="md" fw={500}>
|
|
||||||
Seret gambar atau klik untuk memilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Maksimal 5MB, format gambar wajib
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{previewImage && (
|
|
||||||
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
|
|
||||||
<Image
|
|
||||||
src={previewImage}
|
|
||||||
alt="Preview Gambar"
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
maxHeight: 220,
|
|
||||||
objectFit: "contain",
|
|
||||||
border: `1px solid ${colors["blue-button"]}`,
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text fz="sm" fw="bold">
|
|
||||||
Konten
|
|
||||||
</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={formData.content}
|
|
||||||
onChange={(htmlContent) => {
|
|
||||||
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
|
||||||
beritaState.berita.edit.form.content = htmlContent;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.kategoriBeritaId}
|
value={formData.kategoriBeritaId}
|
||||||
onChange={(val) =>
|
onChange={(val) => handleChange("kategoriBeritaId", val || "")}
|
||||||
setFormData({ ...formData, kategoriBeritaId: val || "" })
|
|
||||||
}
|
|
||||||
label="Kategori"
|
label="Kategori"
|
||||||
placeholder="Pilih kategori"
|
placeholder="Pilih kategori"
|
||||||
data={
|
data={
|
||||||
@@ -236,18 +201,138 @@ function EditBerita() {
|
|||||||
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Deskripsi Singkat
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(htmlContent) =>
|
||||||
|
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Upload Gambar */}
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Berita
|
||||||
|
</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload
|
||||||
|
size={48}
|
||||||
|
color={colors["blue-button"]}
|
||||||
|
stroke={1.5}
|
||||||
|
/>
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 220,
|
||||||
|
objectFit: "contain",
|
||||||
|
border: `1px solid ${colors["blue-button"]}`,
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* Konten */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.content}
|
||||||
|
onChange={(htmlContent) =>
|
||||||
|
setFormData((prev) => ({ ...prev, content: htmlContent }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: "#fff",
|
color: '#fff',
|
||||||
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useProxy } from 'valtio/utils';
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
|
||||||
function DetailBerita() {
|
function DetailBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
@@ -80,7 +80,7 @@ function DetailBerita() {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@@ -111,7 +111,6 @@ function DetailBerita() {
|
|||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Berita" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -124,9 +123,7 @@ function DetailBerita() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Berita" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
|
||||||
@@ -136,7 +133,6 @@ function DetailBerita() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Loader,
|
||||||
|
ActionIcon
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
@@ -29,6 +30,7 @@ export default function CreateBerita() {
|
|||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
beritaState.kategoriBerita.findMany.load();
|
beritaState.kategoriBerita.findMany.load();
|
||||||
@@ -47,6 +49,8 @@ export default function CreateBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
}
|
}
|
||||||
@@ -67,13 +71,18 @@ export default function CreateBerita() {
|
|||||||
|
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/desa/berita/list-berita');
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header dengan tombol kembali */}
|
{/* Header dengan tombol kembali */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -82,7 +91,6 @@ export default function CreateBerita() {
|
|||||||
>
|
>
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Berita
|
Tambah Berita
|
||||||
</Title>
|
</Title>
|
||||||
@@ -131,12 +139,17 @@ export default function CreateBerita() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<Box>
|
||||||
label="Deskripsi Singkat"
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
placeholder="Masukkan deskripsi berita"
|
Deskripsi Singkat
|
||||||
|
</Text>
|
||||||
|
<CreateEditor
|
||||||
value={beritaState.berita.create.form.deskripsi}
|
value={beritaState.berita.create.form.deskripsi}
|
||||||
onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
|
onChange={(htmlContent) => {
|
||||||
|
beritaState.berita.create.form.deskripsi = htmlContent;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
@@ -152,7 +165,7 @@ export default function CreateBerita() {
|
|||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
radius="md"
|
radius="md"
|
||||||
p="xl"
|
p="xl"
|
||||||
>
|
>
|
||||||
@@ -173,7 +186,7 @@ export default function CreateBerita() {
|
|||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm" style={{ textAlign: 'center' }}>
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview Gambar"
|
alt="Preview Gambar"
|
||||||
@@ -185,6 +198,26 @@ export default function CreateBerita() {
|
|||||||
}}
|
}}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Tombol hapus (pojok kanan atas) */}
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -202,6 +235,17 @@ export default function CreateBerita() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -212,7 +256,7 @@ export default function CreateBerita() {
|
|||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
@@ -68,7 +67,6 @@ function ListBerita({ search }: { search: string }) {
|
|||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>Daftar Berita</Title>
|
<Title order={4}>Daftar Berita</Title>
|
||||||
<Tooltip label="Tambah Berita" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconCircleDashedPlus size={18} />}
|
leftSection={<IconCircleDashedPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -77,7 +75,6 @@ function ListBerita({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
@@ -103,7 +102,6 @@ export default function ListImage() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify="space-between" align="center" pt="xs">
|
<Group justify="space-between" align="center" pt="xs">
|
||||||
<Tooltip label="Hapus foto" withArrow>
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -116,7 +114,6 @@ export default function ListImage() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={18} />
|
<IconTrash size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { IconPhoto, IconVideo } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -14,15 +14,13 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
label: "Foto",
|
label: "Foto",
|
||||||
value: "foto",
|
value: "foto",
|
||||||
href: "/admin/desa/gallery/foto",
|
href: "/admin/desa/gallery/foto",
|
||||||
icon: <IconPhoto size={18} stroke={1.8} />,
|
icon: <IconPhoto size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola foto-foto galeri desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Video",
|
label: "Video",
|
||||||
value: "video",
|
value: "video",
|
||||||
href: "/admin/desa/gallery/video",
|
href: "/admin/desa/gallery/video",
|
||||||
icon: <IconVideo size={18} stroke={1.8} />,
|
icon: <IconVideo size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola video galeri desa"
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -70,14 +68,8 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<Tooltip
|
|
||||||
key={i}
|
|
||||||
label={tab.tooltip}
|
|
||||||
position="bottom"
|
|
||||||
withArrow
|
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
|
||||||
>
|
|
||||||
<TabsTab
|
<TabsTab
|
||||||
|
key={i}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
leftSection={tab.icon}
|
leftSection={tab.icon}
|
||||||
style={{
|
style={{
|
||||||
@@ -88,7 +80,6 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTab>
|
</TabsTab>
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -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) {
|
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;
|
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;
|
|
||||||
// }
|
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
@@ -11,11 +12,11 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip
|
Loader
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
||||||
@@ -25,23 +26,38 @@ function EditVideo() {
|
|||||||
const videoState = useProxy(stateGallery.video);
|
const videoState = useProxy(stateGallery.video);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
name: "",
|
||||||
|
deskripsi: "",
|
||||||
|
linkVideo: "",
|
||||||
|
});
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
linkVideo: '',
|
linkVideo: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// load data video sekali saat id ada
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadVideo = async () => {
|
const loadVideo = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await videoState.update.load(id);
|
const data = await videoState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name ?? '',
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi ?? '',
|
||||||
linkVideo: data.linkVideo || '',
|
linkVideo: data.linkVideo ?? '',
|
||||||
|
});
|
||||||
|
setOriginalData({
|
||||||
|
name: data.name ?? '',
|
||||||
|
deskripsi: data.deskripsi ?? '',
|
||||||
|
linkVideo: data.linkVideo ?? '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -49,12 +65,29 @@ function EditVideo() {
|
|||||||
toast.error('Gagal memuat data video');
|
toast.error('Gagal memuat data video');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadVideo();
|
loadVideo();
|
||||||
}, [params?.id]);
|
}, [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 handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
|
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
|
||||||
if (!converted) {
|
if (!converted) {
|
||||||
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
|
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
|
||||||
@@ -63,7 +96,6 @@ function EditVideo() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
videoState.update.form = {
|
videoState.update.form = {
|
||||||
...videoState.update.form,
|
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
deskripsi: formData.deskripsi,
|
deskripsi: formData.deskripsi,
|
||||||
linkVideo: formData.linkVideo,
|
linkVideo: formData.linkVideo,
|
||||||
@@ -75,16 +107,27 @@ function EditVideo() {
|
|||||||
console.error('Error updating video:', error);
|
console.error('Error updating video:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui video');
|
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 (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Video
|
Edit Video
|
||||||
</Title>
|
</Title>
|
||||||
@@ -103,7 +146,7 @@ function EditVideo() {
|
|||||||
label="Judul Video"
|
label="Judul Video"
|
||||||
placeholder="Masukkan judul video"
|
placeholder="Masukkan judul video"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => handleChange('name', e.currentTarget.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -112,11 +155,11 @@ function EditVideo() {
|
|||||||
label="Link Video YouTube"
|
label="Link Video YouTube"
|
||||||
placeholder="https://www.youtube.com/watch?v=abc123"
|
placeholder="https://www.youtube.com/watch?v=abc123"
|
||||||
value={formData.linkVideo}
|
value={formData.linkVideo}
|
||||||
onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })}
|
onChange={(e) => handleChange('linkVideo', e.currentTarget.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
{embedLink && (
|
{embedLink && (
|
||||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
<Box mt="sm" pos="relative" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<iframe
|
<iframe
|
||||||
className="rounded"
|
className="rounded"
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -124,7 +167,27 @@ function EditVideo() {
|
|||||||
src={embedLink}
|
src={embedLink}
|
||||||
title="Preview Video"
|
title="Preview Video"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
></iframe>
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
linkVideo: '',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -135,11 +198,22 @@ function EditVideo() {
|
|||||||
</Title>
|
</Title>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
onChange={(val) => handleChange('deskripsi', val)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -150,7 +224,7 @@ function EditVideo() {
|
|||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
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 { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -102,6 +102,7 @@ function DetailVideo() {
|
|||||||
fz="md"
|
fz="md"
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>
|
<Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>
|
||||||
@@ -110,7 +111,6 @@ function DetailVideo() {
|
|||||||
|
|
||||||
{/* Tombol Aksi */}
|
{/* Tombol Aksi */}
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Video" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -123,9 +123,7 @@ function DetailVideo() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Video" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -137,7 +135,6 @@ function DetailVideo() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
@@ -11,9 +12,9 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Loader
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -25,6 +26,7 @@ function CreateVideo() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [link, setLink] = useState('');
|
const [link, setLink] = useState('');
|
||||||
const embedLink = convertYoutubeUrlToEmbed(link);
|
const embedLink = convertYoutubeUrlToEmbed(link);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
videoState.create.form = {
|
videoState.create.form = {
|
||||||
@@ -36,6 +38,8 @@ function CreateVideo() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
if (!embedLink) {
|
if (!embedLink) {
|
||||||
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
|
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
|
||||||
return;
|
return;
|
||||||
@@ -45,13 +49,18 @@ function CreateVideo() {
|
|||||||
await videoState.create.create();
|
await videoState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/desa/gallery/video');
|
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 (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header Back Button + Title */}
|
{/* Header Back Button + Title */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -60,7 +69,6 @@ function CreateVideo() {
|
|||||||
>
|
>
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Video
|
Tambah Video
|
||||||
</Title>
|
</Title>
|
||||||
@@ -98,7 +106,7 @@ function CreateVideo() {
|
|||||||
|
|
||||||
{/* Preview Video */}
|
{/* Preview Video */}
|
||||||
{embedLink && (
|
{embedLink && (
|
||||||
<Box mt="sm">
|
<Box mt="sm" pos="relative">
|
||||||
<iframe
|
<iframe
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
@@ -109,7 +117,24 @@ function CreateVideo() {
|
|||||||
src={embedLink}
|
src={embedLink}
|
||||||
title="Preview Video"
|
title="Preview Video"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
></iframe>
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setLink('');
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -128,6 +153,17 @@ function CreateVideo() {
|
|||||||
|
|
||||||
{/* Button Submit */}
|
{/* Button Submit */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -138,7 +174,7 @@ function CreateVideo() {
|
|||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
@@ -74,7 +73,6 @@ function ListVideo({ search }: { search: string }) {
|
|||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>Daftar Video</Title>
|
<Title order={4}>Daftar Video</Title>
|
||||||
<Tooltip label="Tambah Video Baru" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -83,7 +81,6 @@ function ListVideo({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
'use client'
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -24,16 +24,29 @@ function EditAjukanPermohonan() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
|
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
nama: stateAjukan.edit.form.nama,
|
|
||||||
nik: stateAjukan.edit.form.nik,
|
const [originalData, setOriginalData] = useState({
|
||||||
alamat: stateAjukan.edit.form.alamat,
|
nama: "",
|
||||||
nomorKk: stateAjukan.edit.form.nomorKk,
|
nik: "",
|
||||||
kategoriId: stateAjukan.edit.form.kategoriId,
|
alamat: "",
|
||||||
|
nomorKk: "",
|
||||||
|
kategoriId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// State lokal form
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
nama: '',
|
||||||
|
nik: '',
|
||||||
|
alamat: '',
|
||||||
|
nomorKk: '',
|
||||||
|
kategoriId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load data awal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateLayananDesa.suratKeterangan.findManyAll.load();
|
stateLayananDesa.suratKeterangan.findManyAll.load();
|
||||||
|
|
||||||
const loadAjukan = async () => {
|
const loadAjukan = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -48,6 +61,13 @@ function EditAjukanPermohonan() {
|
|||||||
nomorKk: data.nomorKk || '',
|
nomorKk: data.nomorKk || '',
|
||||||
kategoriId: data.kategoriId || '',
|
kategoriId: data.kategoriId || '',
|
||||||
});
|
});
|
||||||
|
setOriginalData({
|
||||||
|
nama: data.nama || '',
|
||||||
|
nik: data.nik || '',
|
||||||
|
alamat: data.alamat || '',
|
||||||
|
nomorKk: data.nomorKk || '',
|
||||||
|
kategoriId: data.kategoriId || '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading ajukan:', error);
|
console.error('Error loading ajukan:', error);
|
||||||
@@ -58,8 +78,28 @@ function EditAjukanPermohonan() {
|
|||||||
loadAjukan();
|
loadAjukan();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
|
// Handler untuk input controlled
|
||||||
|
const handleChange = (field: string, value: string) => {
|
||||||
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
stateAjukan.edit.form = {
|
stateAjukan.edit.form = {
|
||||||
...stateAjukan.edit.form,
|
...stateAjukan.edit.form,
|
||||||
...formData,
|
...formData,
|
||||||
@@ -69,6 +109,8 @@ function EditAjukanPermohonan() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating ajukan:', error);
|
console.error('Error updating ajukan:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui ajukan');
|
toast.error('Terjadi kesalahan saat memperbarui ajukan');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,11 +118,9 @@ function EditAjukanPermohonan() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Ajukan Permohonan
|
Edit Ajukan Permohonan
|
||||||
</Title>
|
</Title>
|
||||||
@@ -99,7 +139,7 @@ function EditAjukanPermohonan() {
|
|||||||
label="Nama"
|
label="Nama"
|
||||||
placeholder="Masukkan nama"
|
placeholder="Masukkan nama"
|
||||||
value={formData.nama}
|
value={formData.nama}
|
||||||
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
onChange={(e) => handleChange('nama', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -108,7 +148,7 @@ function EditAjukanPermohonan() {
|
|||||||
label="NIK"
|
label="NIK"
|
||||||
placeholder="Masukkan NIK"
|
placeholder="Masukkan NIK"
|
||||||
value={formData.nik}
|
value={formData.nik}
|
||||||
onChange={(e) => setFormData({ ...formData, nik: e.target.value })}
|
onChange={(e) => handleChange('nik', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -116,7 +156,7 @@ function EditAjukanPermohonan() {
|
|||||||
label="Alamat"
|
label="Alamat"
|
||||||
placeholder="Masukkan alamat"
|
placeholder="Masukkan alamat"
|
||||||
value={formData.alamat}
|
value={formData.alamat}
|
||||||
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
|
onChange={(e) => handleChange('alamat', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -125,7 +165,7 @@ function EditAjukanPermohonan() {
|
|||||||
label="Nomor KK"
|
label="Nomor KK"
|
||||||
placeholder="Masukkan nomor KK"
|
placeholder="Masukkan nomor KK"
|
||||||
value={formData.nomorKk}
|
value={formData.nomorKk}
|
||||||
onChange={(e) => setFormData({ ...formData, nomorKk: e.target.value })}
|
onChange={(e) => handleChange('nomorKk', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -137,18 +177,7 @@ function EditAjukanPermohonan() {
|
|||||||
value: item.id,
|
value: item.id,
|
||||||
}))}
|
}))}
|
||||||
value={formData.kategoriId || null}
|
value={formData.kategoriId || null}
|
||||||
onChange={(val: string | null) => {
|
onChange={(val) => handleChange('kategoriId', val || '')}
|
||||||
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
|
searchable
|
||||||
clearable
|
clearable
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
nothingFoundMessage="Tidak ditemukan"
|
||||||
@@ -156,6 +185,17 @@ function EditAjukanPermohonan() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -166,7 +206,7 @@ function EditAjukanPermohonan() {
|
|||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
@@ -79,7 +78,7 @@ function DetailAjukanPermohonan() {
|
|||||||
<Text fz="lg" fw="bold">
|
<Text fz="lg" fw="bold">
|
||||||
Nama
|
Nama
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="md" c="dimmed">
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>
|
||||||
{data?.nama || '-'}
|
{data?.nama || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -97,7 +96,7 @@ function DetailAjukanPermohonan() {
|
|||||||
<Text fz="lg" fw="bold">
|
<Text fz="lg" fw="bold">
|
||||||
Alamat
|
Alamat
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="md" c="dimmed">
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>
|
||||||
{data?.alamat || '-'}
|
{data?.alamat || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -121,7 +120,6 @@ function DetailAjukanPermohonan() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Surat" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -135,9 +133,7 @@ function DetailAjukanPermohonan() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Surat" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -151,7 +147,6 @@ function DetailAjukanPermohonan() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Pelayanan Penduduk Non Permanent
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
|
||||||
|
|
||||||
|
{/* Nama Field */}
|
||||||
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleChange('name')(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi Field */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={handleChange('deskripsi')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<Group justify="right">
|
||||||
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPelayananPendudukNonPermanent;
|
||||||
@@ -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 (
|
|
||||||
<Box>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Group mb="md">
|
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
|
||||||
Edit Pelayanan Penduduk Non Permanent
|
|
||||||
</Title>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Paper
|
|
||||||
w={{ base: "100%", md: "50%" }}
|
|
||||||
bg={colors['white-1']}
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
shadow="sm"
|
|
||||||
style={{ border: '1px solid #e0e0e0' }}
|
|
||||||
>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
|
|
||||||
|
|
||||||
{/* Nama Field */}
|
|
||||||
<TextInput
|
|
||||||
label="Judul"
|
|
||||||
placeholder="Masukkan judul"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, name: e.target.value })
|
|
||||||
}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Posisi Field */}
|
|
||||||
<Box>
|
|
||||||
<Text fz="sm" fw="bold">
|
|
||||||
Deskripsi
|
|
||||||
</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={formData.deskripsi}
|
|
||||||
onChange={(htmlContent) => {
|
|
||||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
bg={colors['blue-button']}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
loading={statePendudukNonPermanent.update.loading}
|
|
||||||
disabled={!formData.name}
|
|
||||||
>
|
|
||||||
{statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
disabled={statePendudukNonPermanent.update.loading}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditPelayananPendudukNonPermanent;
|
|
||||||
@@ -11,8 +11,7 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
@@ -27,7 +26,7 @@ function PelayananPendudukNonPermanent() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pelayananPendudukNonPermanen.findById.load('1');
|
pelayananPendudukNonPermanen.findById.load('edit');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!pelayananPendudukNonPermanen.findById.data) {
|
if (!pelayananPendudukNonPermanen.findById.data) {
|
||||||
@@ -51,7 +50,6 @@ function PelayananPendudukNonPermanent() {
|
|||||||
</Title>
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Data Pelayanan" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -59,13 +57,12 @@ function PelayananPendudukNonPermanent() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
'/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit'
|
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -91,6 +88,7 @@ function PelayananPendudukNonPermanent() {
|
|||||||
ta="justify"
|
ta="justify"
|
||||||
fz={{ base: '1rem', md: '1.2rem' }}
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Stack align="center" justify="center" py="xl">
|
||||||
|
<Skeleton height={800} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{/* Header */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Pelayanan Perizinan Berusaha
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleChange('name')(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Link"
|
||||||
|
placeholder="Masukkan link terkait"
|
||||||
|
value={formData.link}
|
||||||
|
onChange={(e) => handleChange('link')(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Title order={6}>Deskripsi</Title>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={handleChange('deskripsi')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group justify="right">
|
||||||
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPelayananPerizinanBerusaha;
|
||||||
@@ -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 (
|
|
||||||
<Box>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{/* Header Section */}
|
|
||||||
<Group mb="md">
|
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
|
||||||
Edit Pelayanan Perizinan Berusaha
|
|
||||||
</Title>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* Form Section */}
|
|
||||||
<Paper
|
|
||||||
w={{ base: "100%", md: "50%" }}
|
|
||||||
bg={colors['white-1']}
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
shadow="sm"
|
|
||||||
style={{ border: '1px solid #e0e0e0' }}
|
|
||||||
>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
|
|
||||||
|
|
||||||
{/* Nama Field */}
|
|
||||||
<TextInput
|
|
||||||
label="Judul"
|
|
||||||
placeholder="Masukkan judul"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Link Field */}
|
|
||||||
<TextInput
|
|
||||||
label="Link"
|
|
||||||
placeholder="Masukkan link terkait"
|
|
||||||
value={formData.link}
|
|
||||||
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Deskripsi Field */}
|
|
||||||
<Box>
|
|
||||||
<Title order={6}>Deskripsi</Title>
|
|
||||||
<EditEditor
|
|
||||||
value={formData.deskripsi}
|
|
||||||
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
bg={colors['blue-button']}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
loading={statePerizinanBerusaha.update.loading}
|
|
||||||
disabled={!formData.name}
|
|
||||||
>
|
|
||||||
{statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
disabled={statePerizinanBerusaha.update.loading}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditPelayananPerizinanBerusaha;
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
@@ -15,39 +16,61 @@ import {
|
|||||||
StepperCompleted,
|
StepperCompleted,
|
||||||
StepperStep,
|
StepperStep,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function PerizinanBerusaha() {
|
function PerizinanBerusaha() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pelayananPerizinanBerusaha = useProxy(
|
const pelayananPerizinanBerusaha = useProxy(
|
||||||
stateLayananDesa.pelayananPerizinanBerusaha
|
stateLayananDesa.pelayananPerizinanBerusaha
|
||||||
);
|
);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
const [active, setActive] = useState(1);
|
const [active, setActive] = useState(1);
|
||||||
const nextStep = () =>
|
const nextStep = () =>
|
||||||
setActive((current) => (current < 6 ? current + 1 : current));
|
setActive((current) => (current < 6 ? current + 1 : current));
|
||||||
const prevStep = () =>
|
const prevStep = () =>
|
||||||
setActive((current) => (current > 0 ? current - 1 : current));
|
setActive((current) => (current > 0 ? current - 1 : current));
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useEffect(() => {
|
||||||
pelayananPerizinanBerusaha.findById.load('1');
|
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 (
|
return (
|
||||||
<Stack align="center" justify="center" py="xl">
|
<Stack align="center" justify="center" py="xl">
|
||||||
<Skeleton radius="md" height={800} />
|
<Skeleton height={800} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error || !pelayananPerizinanBerusaha.findById.data) {
|
||||||
|
return (
|
||||||
|
<Center h={200}>
|
||||||
|
<Text>{error || 'Data tidak ditemukan'}</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const data = pelayananPerizinanBerusaha.findById.data;
|
const data = pelayananPerizinanBerusaha.findById.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,7 +84,6 @@ function PerizinanBerusaha() {
|
|||||||
</Title>
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Data Perizinan" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -69,13 +91,12 @@ function PerizinanBerusaha() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
'/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
|
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -101,6 +122,7 @@ function PerizinanBerusaha() {
|
|||||||
ta="justify"
|
ta="justify"
|
||||||
fz={{ base: '1rem', md: '1.2rem' }}
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
@@ -182,3 +204,4 @@ function PerizinanBerusaha() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default PerizinanBerusaha;
|
export default PerizinanBerusaha;
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +1,178 @@
|
|||||||
'use client'
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
'use client';
|
||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// 🔹 Types
|
||||||
|
interface FormData {
|
||||||
|
name: string;
|
||||||
|
deskripsi: string;
|
||||||
|
imageId: string;
|
||||||
|
image2Id: string;
|
||||||
|
imageUrl: string;
|
||||||
|
image2Url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileUploaderProps {
|
||||||
|
title: string;
|
||||||
|
file: File | null;
|
||||||
|
setFile: React.Dispatch<React.SetStateAction<File | null>>;
|
||||||
|
preview: string | null;
|
||||||
|
setPreview: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 File Uploader Component
|
||||||
|
const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
|
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 (
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format .png, .jpg, .jpeg, .webp
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{preview && (
|
||||||
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={preview}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={handleRemove}
|
||||||
|
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔹 Main Component
|
||||||
function EditSuratKeterangan() {
|
function EditSuratKeterangan() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
|
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
// 🧩 State
|
||||||
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
|
const [formData, setFormData] = useState<FormData>({
|
||||||
|
name: '',
|
||||||
|
deskripsi: '',
|
||||||
|
imageId: '',
|
||||||
|
image2Id: '',
|
||||||
|
imageUrl: '',
|
||||||
|
image2Url: '',
|
||||||
|
});
|
||||||
|
const [originalData, setOriginalData] = useState<FormData>(formData);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [file2, setFile2] = useState<File | null>(null);
|
const [file2, setFile2] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
name: stateSurat.edit.form.name,
|
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
|
||||||
deskripsi: stateSurat.edit.form.deskripsi,
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
imageId: stateSurat.edit.form.imageId,
|
|
||||||
image2Id: stateSurat.edit.form.image2Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// 🧭 Load Initial Data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSurat = async () => {
|
const loadSurat = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await stateSurat.edit.load(id);
|
const data = await stateLayananDesa.suratKeterangan.edit.load(id);
|
||||||
if (data) {
|
if (!data) return;
|
||||||
setFormData({
|
|
||||||
|
const mapped: FormData = {
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || '',
|
||||||
imageId: data.imageId || '',
|
imageId: data.imageId || '',
|
||||||
image2Id: data.image2Id || '',
|
image2Id: data.image2Id || '',
|
||||||
});
|
imageUrl: data.image?.link || '',
|
||||||
|
image2Url: data.image2?.link || ''
|
||||||
|
};
|
||||||
|
|
||||||
setPreviewImage(data.image?.link || null);
|
setFormData(mapped);
|
||||||
setPreviewImage2(data.image2?.link || null);
|
setOriginalData(mapped);
|
||||||
}
|
|
||||||
|
if (data.image?.link) setPreviewImage(data.image.link);
|
||||||
|
if (data.image2?.link) setPreviewImage2(data.image2.link);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading surat:', error);
|
console.error('Error loading surat:', error);
|
||||||
toast.error('Gagal memuat data surat');
|
toast.error('Gagal memuat data surat');
|
||||||
@@ -66,50 +182,99 @@ function EditSuratKeterangan() {
|
|||||||
loadSurat();
|
loadSurat();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
// 📤 Upload File Helper
|
||||||
|
const uploadFile = async (file: File): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
stateSurat.edit.form = {
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
...stateSurat.edit.form,
|
file,
|
||||||
...formData,
|
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) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
const uploadedId = await uploadFile(file);
|
||||||
const uploaded = res.data?.data;
|
if (!uploadedId) {
|
||||||
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
toast.error('Gagal upload gambar pertama');
|
||||||
stateSurat.edit.form.imageId = uploaded.id;
|
return;
|
||||||
|
}
|
||||||
|
originalState.edit.form.imageId = uploadedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload file 2 if exists
|
||||||
if (file2) {
|
if (file2) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
|
const uploadedId = await uploadFile(file2);
|
||||||
const uploaded = res.data?.data;
|
if (!uploadedId) {
|
||||||
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
toast.error('Gagal upload gambar kedua');
|
||||||
stateSurat.edit.form.image2Id = uploaded.id;
|
return;
|
||||||
|
}
|
||||||
|
originalState.edit.form.image2Id = uploadedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await stateSurat.edit.update();
|
// Submit update
|
||||||
|
await originalState.edit.update();
|
||||||
toast.success('Surat berhasil diperbarui!');
|
toast.success('Surat berhasil diperbarui!');
|
||||||
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
|
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating surat:', error);
|
console.error('Error updating surat:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui surat');
|
toast.error('Terjadi kesalahan saat memperbarui surat');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
}, [formData, file, file2, router]);
|
||||||
|
|
||||||
|
// 📝 Form Field Handlers
|
||||||
|
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData(prev => ({ ...prev, name: e.target.value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeskripsiChange = (html: string) => {
|
||||||
|
setFormData(prev => ({ ...prev, deskripsi: html }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Back Button */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Surat Keterangan
|
Edit Surat Keterangan
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: '100%', md: '50%' }}
|
w={{ base: '100%', md: '50%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
@@ -119,150 +284,66 @@ function EditSuratKeterangan() {
|
|||||||
style={{ border: '1px solid #e0e0e0' }}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
|
{/* Nama Surat */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nama Surat Keterangan"
|
label="Nama Surat Keterangan"
|
||||||
placeholder="Masukkan nama surat keterangan"
|
placeholder="Masukkan nama surat keterangan"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={handleNameChange}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="sm" fw="bold" mb={6}>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
Konten
|
Konten
|
||||||
</Text>
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
|
onChange={handleDeskripsiChange}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Upload Gambar 1 */}
|
{/* Gambar 1 */}
|
||||||
<Box>
|
<FileUploader
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
title="Gambar Konten Pelayanan"
|
||||||
Gambar Konten Pelayanan
|
file={file}
|
||||||
</Text>
|
setFile={setFile}
|
||||||
<Dropzone
|
preview={previewImage}
|
||||||
onDrop={(files) => {
|
setPreview={setPreviewImage}
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={180}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={48} color="red" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
<Stack gap="xs" align="center">
|
|
||||||
<Text size="md" fw={500}>
|
|
||||||
Seret gambar atau klik untuk memilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Maksimal 5MB, format gambar wajib
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{previewImage && (
|
|
||||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Image
|
|
||||||
src={previewImage}
|
|
||||||
alt="Preview Gambar 1"
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
maxHeight: 220,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: `1px solid ${colors['blue-button']}`,
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Upload Gambar 2 */}
|
{/* Gambar 2 */}
|
||||||
<Box>
|
<FileUploader
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
title="Gambar Alur Pelayanan Surat"
|
||||||
Gambar Alur Pelayanan Surat
|
file={file2}
|
||||||
</Text>
|
setFile={setFile2}
|
||||||
<Dropzone
|
preview={previewImage2}
|
||||||
onDrop={(files) => {
|
setPreview={setPreviewImage2}
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={180}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={48} color="red" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
<Stack gap="xs" align="center">
|
|
||||||
<Text size="md" fw={500}>
|
|
||||||
Seret gambar atau klik untuk memilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Maksimal 5MB, format gambar wajib
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{previewImage2 && (
|
|
||||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Image
|
|
||||||
src={previewImage2}
|
|
||||||
alt="Preview Gambar 2"
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
maxHeight: 220,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: `1px solid ${colors['blue-button']}`,
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
disabled={isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79,172,254,0.4)',
|
boxShadow: '0 4px 15px rgba(79,172,254,0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
@@ -95,6 +94,7 @@ function DetailSuratKeterangan() {
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: data?.deskripsi || '-',
|
__html: data?.deskripsi || '-',
|
||||||
}}
|
}}
|
||||||
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -141,7 +141,6 @@ function DetailSuratKeterangan() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Surat" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -155,9 +154,7 @@ function DetailSuratKeterangan() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Surat" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -171,7 +168,6 @@ function DetailSuratKeterangan() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
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 [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
|
||||||
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateSurat.create.form = {
|
stateSurat.create.form = {
|
||||||
@@ -46,6 +48,7 @@ function CreateSuratKeterangan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
// Upload gambar utama
|
// Upload gambar utama
|
||||||
const res1 = await ApiFetch.api.fileStorage.create.post({
|
const res1 = await ApiFetch.api.fileStorage.create.post({
|
||||||
file: previewImage.file,
|
file: previewImage.file,
|
||||||
@@ -78,6 +81,8 @@ function CreateSuratKeterangan() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating surat keterangan:', error);
|
console.error('Error creating surat keterangan:', error);
|
||||||
toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
|
toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,11 +90,9 @@ function CreateSuratKeterangan() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Surat Keterangan
|
Tambah Surat Keterangan
|
||||||
</Title>
|
</Title>
|
||||||
@@ -108,7 +111,7 @@ function CreateSuratKeterangan() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={stateSurat.create.form.name}
|
value={stateSurat.create.form.name}
|
||||||
onChange={(val) => (stateSurat.create.form.name = val.target.value)}
|
onChange={(val) => (stateSurat.create.form.name = val.target.value)}
|
||||||
label={<Text fz="sm" fw="bold">Nama Surat Keterangan</Text>}
|
label="Nama Surat Keterangan"
|
||||||
placeholder="Masukkan nama surat keterangan"
|
placeholder="Masukkan nama surat keterangan"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -143,7 +146,7 @@ function CreateSuratKeterangan() {
|
|||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
radius="md"
|
radius="md"
|
||||||
p="xl"
|
p="xl"
|
||||||
>
|
>
|
||||||
@@ -164,7 +167,7 @@ function CreateSuratKeterangan() {
|
|||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm" style={{ textAlign: 'center' }}>
|
<Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage.preview}
|
src={previewImage.preview}
|
||||||
alt="Preview Gambar Utama"
|
alt="Preview Gambar Utama"
|
||||||
@@ -172,6 +175,23 @@ function CreateSuratKeterangan() {
|
|||||||
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -193,7 +213,7 @@ function CreateSuratKeterangan() {
|
|||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
radius="md"
|
radius="md"
|
||||||
p="xl"
|
p="xl"
|
||||||
>
|
>
|
||||||
@@ -214,7 +234,7 @@ function CreateSuratKeterangan() {
|
|||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage2 ? (
|
{previewImage2 ? (
|
||||||
<Box mt="sm" style={{ textAlign: 'center' }}>
|
<Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage2.preview}
|
src={previewImage2.preview}
|
||||||
alt="Preview Gambar Tambahan"
|
alt="Preview Gambar Tambahan"
|
||||||
@@ -222,6 +242,23 @@ function CreateSuratKeterangan() {
|
|||||||
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage2(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Text size="sm" c="dimmed" mt="sm" ta="center">
|
<Text size="sm" c="dimmed" mt="sm" ta="center">
|
||||||
@@ -232,6 +269,17 @@ function CreateSuratKeterangan() {
|
|||||||
|
|
||||||
{/* Tombol Simpan */}
|
{/* Tombol Simpan */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -242,7 +290,7 @@ function CreateSuratKeterangan() {
|
|||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -82,7 +81,6 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
|||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>List Surat Keterangan</Title>
|
<Title order={4}>List Surat Keterangan</Title>
|
||||||
<Tooltip label="Tambah Surat Keterangan" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -93,7 +91,6 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
@@ -119,6 +116,7 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
|||||||
<Box w={200}>
|
<Box w={200}>
|
||||||
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
|
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
|
||||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user