Compare commits

...

24 Commits

Author SHA1 Message Date
d66a952d4c Merge pull request 'nico/27-okt-25' (#1) from nico/27-okt-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/1
2025-10-27 22:17:59 +08:00
ed371bd0d9 Fix QC Kak Inno 24 Okt 25
Fix QC Kak Ayu 24 Okt 25
Fix QC Keano 24 Okt 25
Fix Detail Lowongan Kerja
2025-10-27 22:15:55 +08:00
f82c7b86e0 27 Oct 2025-10-27 10:54:50 +08:00
b5d6585cd5 27 Oct 2025-10-27 10:54:01 +08:00
aa98359ef7 Fix Revisi Kak Inno 22 Oktober && Fix Revisi Kak Ayu 22 Oktober 2025-10-23 17:45:45 +08:00
0ff0d5234a Fix QC Kak Inno 21 Oktober, QC Kak Ayu 21 Oktober, QC Keano, && QC Pak Jun 21 Oktober 2025-10-22 17:00:12 +08:00
827c1c191a Revisi QC Kak Inno tanggal 20 2025-10-22 09:58:16 +08:00
fb596f9033 Fix QC Kak Inno 17 Okt 25, Fix QC Kak Ayu 17 Okt 25, & Fix Qc Pak Jun 17 Okt 25 2025-10-21 12:17:30 +08:00
9055b40769 Fix navbar mobile add active page 2025-10-19 18:08:49 +08:00
bbf13c1cf7 Mengerjakan QC Kak Inno & Kak Ayu Tanggal 16 Oktober
Fix Search
2025-10-17 17:45:56 +08:00
75bf0652b1 Fix QC Kak Inno & Kak Ayu Tanggal 15 Oct 2025-10-17 10:03:03 +08:00
0b574406e2 Fix QC Kak Inno : tanggal 14 Oktober
Fitur Search bisa digunakan di 6 Menu, sisa 3 Menu Lagi
2025-10-15 17:29:57 +08:00
ccf39bc778 Penambahan fungsi search disetiap menu & submenu,
Menu Landing Page
Menu PPID
Menu Desa
2025-10-15 10:13:02 +08:00
3c21f7742c Yang sudh dikerjakan:
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya ( status buku bisa engga otomatis dipinjemnya), kalau dikembalikan statusnya otomatis
)
Yang Mau Dikerjakan:
Cek fungsi menu yang kompleks
2025-10-14 10:38:55 +08:00
a158241c0b - QC User & Admin Menu Pendidikan V
- Fix SubMenu :
- Beasiswa Desa ( Baca Selengkapnya terdapatkan konten ) V
- Info Sekolah ( Kategori Menyesuaikan Dengan Datanya ) V
- Perpustakaan Digital (  V
- Kategori Menyesuaikan Dengan Datanya V
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya V
)
2025-10-13 11:20:38 +08:00
80c5dc6361 - QC User & Admin Menu Lingkungan
- Fix SubMenu : Edukasi Lingkungan & Konservasi Adat Bali dibagian User
- Fix SUbMenu : Gotong Royong User ( Tabs kategori menyesuaikan dengan data kategori kegiatan )
2025-10-08 17:06:21 +08:00
8ad38fc907 - QC User & Admin Menu Lingkungan
- Fix SubMenu : Edukasi Lingkungan & Konservasi Adat Bali dibagian User
- Fix SUbMenu : Gotong Royong User ( Tabs kategori menyesuaikan dengan data kategori kegiatan )
2025-10-08 14:02:11 +08:00
d601b2fee3 Fix Bug SubMenu Struktur PPID, SubMenu Struktur Organisasi BumDes 2025-10-07 14:38:20 +08:00
cee0957e07 QC - User & Admin Menu Ekonomi SubMenu Pasar Desa
Fix bug kategori produk
2025-10-06 10:26:59 +08:00
5c66eccf23 Fix Menu Ekonomi :
Pasar Desa : Kategorinya ga tampil,
Bug inputan edit di submenu : Demografi pekerjaa
2025-10-04 21:34:31 +08:00
f7fd9be255 QC User & Admin Responsive : Menu Kesehatan - Ekonomi 2025-10-03 10:17:06 +08:00
8a6d8ed8db QC User & Admin Responsive : Menu Landing Page - Desa 2025-10-02 00:10:33 +08:00
63054cedf0 fix inputan edit menu: desa, ekonomi, inovasi, keamanan, kesehatan, landing-page, & lingkungan 2025-09-30 21:41:26 +08:00
c2f1ab8179 Fix Menu Desa Admin & User 2025-09-30 17:13:06 +08:00
397 changed files with 18262 additions and 9598 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,7 +3,7 @@
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "bun --bun next dev --hostname 0.0.0.0",
"dev": "bun --bun next dev",
"build": "bun --bun next build",
"start": "bun --bun next start"
},
@@ -43,6 +43,7 @@
"@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16",
"@types/nodemailer": "^7.0.2",
"add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
@@ -52,6 +53,7 @@
"classnames": "^2.5.1",
"colors": "^1.4.0",
"dayjs": "^1.11.13",
"dotenv": "^17.2.3",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
@@ -71,6 +73,7 @@
"next": "^15.5.2",
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
"nodemailer": "^7.0.10",
"p-limit": "^6.2.0",
"primeicons": "^7.0.0",
"primereact": "^10.9.6",
@@ -82,6 +85,7 @@
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"sharp": "^0.34.3",

View File

@@ -1,5 +1,4 @@
[
{ "name": "Semua" },
{ "name": "Pemerintahan" },
{ "name": "Pembangunan" },
{ "name": "Ekonomi" },

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"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>"
}

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"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>",
"link" : "https://oss.go.id/"

View File

@@ -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"
}
]

View 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
}
]

View File

@@ -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
}
]

View File

@@ -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"
}
]
]

View File

@@ -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
}
]

View File

@@ -1,16 +1,4 @@
[
{
"id": "cmds8w2q60002vnbe6i8qhkuo",
"name": "Telephone Desa Darmasaba",
"iconUrl": "081239580000",
"imageId": "cmff3nv180003vn6h5jvedidq"
},
{
"id": "cmds8z7u20005vnbegyyvnbk0",
"name": "Email Desa Darmasaba",
"iconUrl": "desadarmasaba@badungkab.go.id",
"imageId": "cmff3ll130001vn6hkhls3f5y"
},
{
"id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba",

View File

@@ -0,0 +1,6 @@
[
{ "nama": "Kebersihan" },
{ "nama": "Infrastruktur" },
{ "nama": "Sosial" },
{ "nama": "Lingkungan" }
]

View File

@@ -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" }
]

View File

@@ -1,6 +1,6 @@
[
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"id": "cmgewz4gt000704ib91i3f169",
"namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.",
"gelarAkademik": "S.H.,M.H.,NL.P.",
"tanggalMasuk": "2020-01-01T00:00:00.000Z",
@@ -11,7 +11,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"id": "cmgewxfvw000004ibee5013f4",
"namaLengkap": "I Ketut Suwanta",
"gelarAkademik": "S.Pt",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -22,7 +22,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440006",
"id": "cmgewxvqw000104ibgm5l8fzs",
"namaLengkap": "Ni Wayan Supardiati",
"gelarAkademik": "S.Pd",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -33,7 +33,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440011",
"id": "cmgewy1g9000204ib2n7hbx0i",
"namaLengkap": "I Wayan Agus Juni Artha Saputra",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -44,7 +44,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440012",
"id": "cmgewybah000304ibgqhn1gm2",
"namaLengkap": "I Wayan Sueca",
"gelarAkademik": "S.H.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -55,7 +55,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440017",
"id": "cmgewygqz000404ib20sv8nvg",
"namaLengkap": "Si Gede Ketut Astawa",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -66,7 +66,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440018",
"id": "cmgewyos1000504ibcu8o2gyk",
"namaLengkap": "I Kadek Arya Minarta",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -77,7 +77,7 @@
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440021",
"id": "cmgewyxk7000604ib8djs3i6c",
"namaLengkap": "I Gede Andika Pradnya Diputra",
"gelarAkademik": "S.E.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",

View File

@@ -81,7 +81,7 @@ model FileStorage {
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
PasarDesa PasarDesa[]
Pegawai Pegawai[]
PegawaiBumDes PegawaiBumDes[]
DesaDigital DesaDigital[]
InfoTekno InfoTekno[]
PengaduanMasyarakat PengaduanMasyarakat[]
@@ -101,6 +101,7 @@ model FileStorage {
MitraKolaborasi MitraKolaborasi[]
ArtikelKesehatan ArtikelKesehatan[]
StrukturBumDes StrukturBumDes[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -142,7 +143,7 @@ model MediaSosial {
isActive Boolean @default(true)
}
//========================================= PROFILE ========================================= //
//========================================= DESA ANTI KORUPSI ========================================= //
model DesaAntiKorupsi {
id String @id @default(cuid())
name String @unique
@@ -298,6 +299,7 @@ model PosisiOrganisasiPPID {
updatedAt DateTime @updatedAt
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent")
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
}
model PegawaiPPID {
@@ -316,15 +318,16 @@ model PegawaiPPID {
updatedAt DateTime @updatedAt
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturPPID[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
}
model StrukturOrganisasiPPID {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String @db.Uuid
hubunganOrganisasiId String @db.Uuid
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
pegawaiId String
hubunganOrganisasiId String
posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id])
pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@ -1167,6 +1170,7 @@ model KontakDarurat {
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
whatsapp String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1340,6 +1344,7 @@ model PasarDesa {
harga Int
rating Float
alamatUsaha String
kontak String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1382,6 +1387,7 @@ model LowonganPekerjaan {
gaji String
deskripsi String
kualifikasi String
notelp String
tanggalPosting DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
@@ -1391,24 +1397,39 @@ model LowonganPekerjaan {
// ========================================= STRUKTUR ORGANISASI ========================================= //
model PosisiOrganisasi {
id String @id @default(uuid()) @db.VarChar(50)
model StrukturBumDes {
id String @id @default(cuid())
name String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
PosisiOrganisasiBumDes PosisiOrganisasiBumDes? @relation(fields: [posisiOrganisasiBumDesId], references: [id])
posisiOrganisasiBumDesId String?
PegawaiBumDes PegawaiBumDes? @relation(fields: [pegawaiBumDesId], references: [id])
pegawaiBumDesId String?
}
model PosisiOrganisasiBumDes {
id String @id @default(cuid())
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
pegawai PegawaiBumDes[]
strukturOrganisasi StrukturBumDes[] // Relasi balik
parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posisi_organisasi")
parent PosisiOrganisasiBumDes? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiBumDes[] @relation("Parent")
StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
}
model Pegawai {
id String @id @default(uuid()) @db.Uuid
model PegawaiBumDes {
id String @id @default(cuid())
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
@@ -1421,49 +1442,22 @@ model Pegawai {
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasi @relation(fields: [posisiId], references: [id])
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@map("pegawai")
posisi PosisiOrganisasiBumDes @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturBumDes[] // Relasi balik
StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
}
model HubunganOrganisasi {
id String @id @default(uuid()) @db.Uuid
atasanId String @db.Uuid
bawahanId String @db.Uuid
tipe String? @db.VarChar(50)
atasan Pegawai @relation("AtasanToBawahan", fields: [atasanId], references: [id])
bawahan Pegawai @relation("BawahanToAtasan", fields: [bawahanId], references: [id])
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
@@unique([atasanId, bawahanId])
@@map("hubungan_organisasi")
}
model StrukturOrganisasi {
model StrukturOrganisasiBumDes {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String @db.Uuid
hubunganOrganisasiId String @db.Uuid
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
hubunganOrganisasi HubunganOrganisasi @relation(fields: [hubunganOrganisasiId], references: [id])
pegawaiId String
hubunganOrganisasiId String
posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id])
pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
@@map("struktur_organisasi")
}
// ========================================= PROGRAM KEMISKINAN ========================================= //
@@ -1612,7 +1606,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
}
// ========================================= INOVASI ========================================= //
// ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital {
id String @id @default(cuid())
@@ -2093,6 +2087,9 @@ model DataPerpustakaan {
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
// relasi baru ke peminjaman
peminjamanBuku PeminjamanBuku[]
}
model KategoriBuku {
@@ -2105,6 +2102,31 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[]
}
model PeminjamanBuku {
id String @id @default(cuid())
nama String
noTelp String
alamat String
bukuId String
tanggalPinjam DateTime @default(now())
batasKembali DateTime // tenggat waktu pengembalian
tanggalKembali DateTime? // diisi saat dikembalikan
status StatusPeminjaman @default(Dipinjam)
catatan String? // opsional, misal: kondisi buku
buku DataPerpustakaan @relation(fields: [bukuId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
enum StatusPeminjaman {
Dipinjam
Dikembalikan
Terlambat
Dibatalkan
}
// ========================================= USER ========================================= //
model User {

View File

@@ -31,14 +31,14 @@ import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
import kategoriBerita from "./data/kategori-berita.json";
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai-bumdes.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
import kategoriBerita from "./data/desa/berita/kategori-berita.json";
import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
import kategoriKegiatanData from "./data/lingkungan/gotong-royong/kategori-gotong-royong.json";
import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
@@ -56,6 +56,7 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
import roles from "./data/user/roles.json";
import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
import seedAssets from "./seed_assets";
import { safeSeedUnique } from "./safeseedUnique";
@@ -807,28 +808,34 @@ import { safeSeedUnique } from "./safeseedUnique";
}
console.log("kategori produk success ...");
for (const p of posisiOrganisasi) {
await prisma.posisiOrganisasi.upsert({
where: {
id: p.id,
},
update: {
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
},
create: {
id: p.id,
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
},
const flattenedPosisiBumdes = posisiOrganisasi.flat();
// ✅ Urutkan berdasarkan hierarki
const sortedPosisiBumdes = flattenedPosisiBumdes.sort((a, b) => a.hierarki - b.hierarki);
for (const p of sortedPosisiBumdes) {
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
if (p.parentId) {
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
if (!parentExists) {
console.warn(
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
);
continue;
}
}
await prisma.posisiOrganisasiBumDes.upsert({
where: { id: p.id },
update: p,
create: p,
});
}
console.log("posisi organisasi success ...");
console.log("posisi organisasi berhasil");
for (const p of pegawai) {
await prisma.pegawai.upsert({
await prisma.pegawaiBumDes.upsert({
where: {
id: p.id,
},
@@ -857,26 +864,6 @@ import { safeSeedUnique } from "./safeseedUnique";
}
console.log("pegawai success ...");
for (const p of hubunganOrganisasi) {
await prisma.hubunganOrganisasi.upsert({
where: {
atasanId_bawahanId: {
atasanId: p.atasanId,
bawahanId: p.bawahanId,
},
},
update: {
tipe: p.tipe,
},
create: {
atasanId: p.atasanId,
bawahanId: p.bawahanId,
tipe: p.tipe,
},
});
}
console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) {
await prisma.detailDataPengangguran.upsert({
where: {
@@ -900,6 +887,30 @@ import { safeSeedUnique } from "./safeseedUnique";
}
console.log("📊 detailDataPengangguran success ...");
// =========== KATEGORI GOTONG ROYONG ===========
// Add IDs to the kategoriKegiatan data
const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
...k,
id: `kategori-${index + 1}`
}));
for (const k of kategoriKegiatan) {
await prisma.kategoriKegiatan.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
},
create: {
id: k.id,
nama: k.nama,
},
});
}
console.log("kategori kegiatan success ...");
for (const e of tujuanEdukasiLingkungan) {
await prisma.tujuanEdukasiLingkungan.upsert({
where: {
@@ -1154,6 +1165,22 @@ import { safeSeedUnique } from "./safeseedUnique";
"✅ 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();

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

View File

@@ -75,7 +75,8 @@ const berita = proxy({
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path
const startTime = Date.now();
berita.findMany.loading = true;
berita.findMany.page = page;
berita.findMany.search = search;
@@ -98,7 +99,14 @@ const berita = proxy({
berita.findMany.data = [];
berita.findMany.totalPages = 1;
} finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
berita.findMany.loading = false;
}, delay);
}
},
},

View File

@@ -581,33 +581,24 @@ const pelayananPerizinanBerusaha = proxy({
findById: {
data: null as pelayananPerizinanBerusahaForm | null,
loading: false,
initialize() {
pelayananPerizinanBerusaha.findById.data = {
id: "",
name: "",
deskripsi: "",
link: "",
} as pelayananPerizinanBerusahaForm;
},
async load(id: string) {
try {
pelayananPerizinanBerusaha.findById.loading = true;
const res = await fetch(
`/api/desa/layanan/pelayananperizinanberusaha/${id}`
);
if (res.ok) {
const data = await res.json();
pelayananPerizinanBerusaha.findById.data = data.data ?? null;
} else {
console.error(
"Failed to fetch pelayanan perizinan berusaha:",
res.statusText
);
pelayananPerizinanBerusaha.findById.data = null;
this.loading = true;
const response = await fetch(`/api/desa/layanan/pelayananperizinanberusaha/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
this.data = result.data; // Make sure this matches your API response structure
}
return result?.data || null;
} catch (error) {
console.error("Error fetching pelayanan perizinan berusaha:", error);
pelayananPerizinanBerusaha.findById.data = null;
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
return null;
} finally {
this.loading = false;
}
},
},

View File

@@ -13,6 +13,7 @@ const templateForm = z.object({
gaji: z.string(),
deskripsi: z.string(),
kualifikasi: z.string(),
notelp: z.string(),
});
const defaultForm = {
@@ -23,6 +24,7 @@ const defaultForm = {
gaji: "",
deskripsi: "",
kualifikasi: "",
notelp: "",
};
const lowonganKerjaState = proxy({
@@ -179,6 +181,7 @@ const lowonganKerjaState = proxy({
gaji: data.gaji,
deskripsi: data.deskripsi,
kualifikasi: data.kualifikasi,
notelp: data.notelp,
};
return data;
} else {
@@ -218,6 +221,7 @@ const lowonganKerjaState = proxy({
gaji: this.form.gaji,
deskripsi: this.form.deskripsi,
kualifikasi: this.form.kualifikasi,
notelp: this.form.notelp,
}),
});
if (!response.ok) {

View File

@@ -12,6 +12,7 @@ const templatePasarDesaForm = z.object({
imageId: z.string().min(1, "Gambar wajib dipilih"),
rating: z.number().min(1, "Rating minimal 1"),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
kontak: z.string().min(1, "Kontak wajib diisi"),
});
const defaultPasarDesaForm = {
@@ -21,6 +22,7 @@ const defaultPasarDesaForm = {
imageId: "",
rating: 0,
kategoriId: [] as string[],
kontak: "",
};
const pasarDesa = proxy({
@@ -188,6 +190,7 @@ const pasarDesa = proxy({
imageId: data.imageId,
rating: data.rating,
kategoriId: data.kategoriId,
kontak: data.kontak,
};
return data;
} else {
@@ -225,6 +228,7 @@ const pasarDesa = proxy({
imageId: this.form.imageId,
rating: this.form.rating,
kategoriId: this.form.kategoriId,
kontak: this.form.kontak,
}),
});
if (!response.ok) {
@@ -336,6 +340,40 @@ const kategoriProduk = proxy({
}
},
},
// ✅ Versi findManyAll (ambil semua tanpa pagination)
findManyAll: {
data: null as
| Prisma.KategoriProdukGetPayload<{
omit: { isActive: true };
}>[]
| null,
loading: false,
search: "",
load: async (search = "") => {
kategoriProduk.findManyAll.loading = true;
kategoriProduk.findManyAll.search = search;
try {
const query: any = {};
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many-all"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kategoriProduk.findManyAll.data = res.data.data ?? [];
} else {
kategoriProduk.findManyAll.data = [];
}
} catch (err) {
console.error("Gagal fetch kategori produk (all):", err);
kategoriProduk.findManyAll.data = [];
} finally {
kategoriProduk.findManyAll.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KategoriProdukGetPayload<{
omit: { isActive: true };

View File

@@ -1,9 +1,173 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { proxy } from "valtio";
import { z } from "zod";
import { toast } from "react-toastify";
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
const defaultForm = {
name: "",
imageId: "",
};
type StrukturBumDesForm = Prisma.StrukturBumDesGetPayload<{
select: {
id: true;
name: true;
imageId: true;
image?: {
select: {
link: true;
};
};
};
}>;
const stateStruktur = proxy({
struktur: {
data: null as StrukturBumDesForm | null,
loading: false,
error: null as string | null,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ekonomi/struktur-organisasi/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
this.data = result.data;
return result.data;
} else {
throw new Error(result.message || "Gagal mengambil data struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data struktur");
return null;
} finally {
this.loading = false;
}
},
reset() {
this.data = null;
this.error = null;
this.loading = false;
},
},
editStruktur: {
id: "",
form: { ...defaultForm },
loading: false,
error: null as string | null,
isReadOnly: false,
initialize(strukturData: StrukturBumDesForm) {
this.id = strukturData.id;
this.isReadOnly = false;
this.form = {
name: strukturData.name || "",
imageId: strukturData.imageId || "",
};
},
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
async submit() {
const validation = templateForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ekonomi/struktur-organisasi/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update struktur");
await stateStruktur.struktur.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat update struktur");
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
},
},
async loadForEdit(id: string) {
const strukturData = await this.struktur.load(id);
if (strukturData) {
this.editStruktur.initialize(strukturData);
}
return strukturData;
},
reset() {
this.struktur.reset();
this.editStruktur.reset();
},
});
const templatePosisiOrganisasi = z.object({
nama: z.string().min(1, "Nama harus diisi"),
@@ -30,9 +194,7 @@ const posisiOrganisasi = proxy({
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"posisi-organisasi"
]["create"].post(this.form);
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['create'].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load();
@@ -52,6 +214,29 @@ const posisiOrganisasi = proxy({
},
},
findUnique: {
data: null as Prisma.StrukturOrganisasiBumDesGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/struktur-organisasi/posisi-organisasi/${id}`
);
if (res.ok) {
const data = await res.json();
posisiOrganisasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch posisiOrganisasi:", res.statusText);
posisiOrganisasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching posisiOrganisasi:", error);
posisiOrganisasi.findUnique.data = null;
}
},
},
edit: {
id: "",
form: { ...posisiOrganisasiDefaultForm },
@@ -165,16 +350,16 @@ const posisiOrganisasi = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
load: async (page = 1, limit?: number, search = "") => {
const appliedLimit = limit ?? 10;
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
try {
const query: any = { page, limit };
const query: any = { page, limit: appliedLimit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many'].get({ query });
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
@@ -192,7 +377,42 @@ const posisiOrganisasi = proxy({
}
},
},
findManyAll: {
data: [] as Array<{
id: string;
nama: string;
deskripsi: string | null;
hierarki: number;
}>,
loading: false,
search: "",
load: async (search = "") => {
// Change to arrow function
posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
posisiOrganisasi.findManyAll.search = search;
try {
const query: any = { search };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many-all'].get({
query,
});
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load posisiOrganisasi:", res.data?.message);
posisiOrganisasi.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading posisiOrganisasi:", error);
posisiOrganisasi.findManyAll.data = [];
} finally {
posisiOrganisasi.findManyAll.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
@@ -231,12 +451,12 @@ const posisiOrganisasi = proxy({
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama wajib diisi"),
gelarAkademik: z.string().optional(),
imageId: z.string().nullable().optional(),
tanggalMasuk: z.string().optional(), // ISO format
gelarAkademik: z.string().min(1, "Gelar Akademik wajib diisi"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ISO format
email: z.string().email("Email tidak valid").optional(),
telepon: z.string().optional(),
alamat: z.string().optional(),
telepon: z.string().min(1, "Telepom wajib diisi"),
alamat: z.string().min(1, "Alamat wajib diisi"),
posisiId: z.string().min(1, "Posisi wajib diisi"),
isActive: z.boolean().default(true),
});
@@ -267,9 +487,9 @@ const pegawai = proxy({
try {
pegawai.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"pegawai"
]["create"].post(pegawai.create.form);
const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['create'].post(
pegawai.create.form
);
if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load();
@@ -286,20 +506,31 @@ const pegawai = proxy({
},
// In struktur-organisasi.ts
findMany: {
data: null as any[] | null,
findMany: {
data: null as
| Prisma.PegawaiBumDesGetPayload<{
include: {
image: true;
posisi: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
total: 0,
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.page = page;
pegawai.findMany.search = search;
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"pegawai"
]["find-many"].get({
query: { page, limit },
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['find-many'].get({
query,
});
if (res.status === 200 && res.data?.success) {
@@ -321,10 +552,10 @@ findMany: {
pegawai.findMany.loading = false;
}
},
},
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
| (Prisma.PegawaiBumDesGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,
@@ -350,12 +581,9 @@ findMany: {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
const res = await fetch(
`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`,
{
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`, {
method: "DELETE",
}
);
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai");
@@ -372,6 +600,31 @@ findMany: {
},
},
nonActive: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.nonActive.loading = true;
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/non-active/${id}`, {
method: "DELETE", // biasanya nonActive pakai PATCH
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
await pegawai.findMany.load(); // refresh data
} else {
toast.error(json.message ?? "Gagal menonaktifkan pegawai");
}
} catch (error) {
console.error("Gagal nonActive:", error);
toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
} finally {
pegawai.nonActive.loading = false;
}
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },
@@ -384,15 +637,12 @@ findMany: {
}
try {
const response = await fetch(
`/api/ekonomi/struktur-organisasi/pegawai/${id}`,
{
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -503,299 +753,10 @@ findMany: {
},
});
// Schema Zod untuk form validasi
const templateHubunganOrganisasiForm = z.object({
atasanId: z.string().min(1, "Atasan wajib dipilih"),
bawahanId: z.string().min(1, "Bawahan wajib dipilih"),
tipe: z.string().optional(),
});
// Default form state
const defaultHubunganOrganisasiForm = {
atasanId: "",
bawahanId: "",
tipe: "",
};
// ====================== STATE ===========================
const hubunganOrganisasi = proxy({
create: {
form: { ...defaultHubunganOrganisasiForm },
loading: false,
async create() {
const cek = templateHubunganOrganisasiForm.safeParse(
hubunganOrganisasi.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n")}]`;
return toast.error(err);
}
try {
hubunganOrganisasi.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"hubungan-organisasi"
]["create"].post(hubunganOrganisasi.create.form);
if (res.status === 200 && res.data?.success) {
hubunganOrganisasi.findMany.load();
return toast.success("Berhasil menambahkan hubungan organisasi");
} else {
return toast.error(res.data?.message || "Gagal menambahkan data");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan");
} finally {
hubunganOrganisasi.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
atasanId: string;
bawahanId: string;
tipe?: string | null;
atasan: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string | null;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
bawahan: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string | null;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
}> | null,
async load() {
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"hubungan-organisasi"
]["find-many"].get();
if (res.status === 200) {
hubunganOrganisasi.findMany.data = (res.data?.data ?? []).map(
(item: any) => ({
...item,
atasan: item.atasan
? {
...item.atasan,
isActive: item.atasan.isActive ?? item.atasan.aktif ?? true,
}
: null,
bawahan: item.bawahan
? {
...item.bawahan,
isActive:
item.bawahan.isActive ?? item.bawahan.aktif ?? true,
}
: null,
})
);
} else {
hubunganOrganisasi.findMany.data = [];
}
} catch (error) {
console.error("Fetch list error:", error);
toast.error("Gagal memuat data hubungan organisasi");
hubunganOrganisasi.findMany.data = [];
}
},
},
findUnique: {
data: null as {
id: string;
atasanId: string;
bawahanId: string;
tipe?: string | null;
atasan?: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
aktif: boolean;
createdAt: Date;
updatedAt: Date;
};
bawahan?: {
id: string;
namaLengkap: string;
gelarAkademik: string | null;
imageId: string;
tanggalMasuk: Date | null;
email: string | null;
telepon: string | null;
alamat: string | null;
posisiId: string;
aktif: boolean;
createdAt: Date;
updatedAt: Date;
};
} | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
);
const result = await res.json();
if (res.ok && result?.success) {
hubunganOrganisasi.findUnique.data = result.data;
} else {
hubunganOrganisasi.findUnique.data = null;
toast.error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Find unique error:", error);
hubunganOrganisasi.findUnique.data = null;
}
},
},
edit: {
id: "",
form: { ...defaultHubunganOrganisasiForm },
loading: false,
async load(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
);
const result = await res.json();
if (res.ok && result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
atasanId: data.atasanId,
bawahanId: data.bawahanId,
tipe: data.tipe || "",
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateHubunganOrganisasiForm.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n")}]`;
return toast.error(err);
}
try {
this.loading = true;
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await res.json();
if (res.ok && result.success) {
await hubunganOrganisasi.findMany.load();
toast.success("Berhasil mengupdate hubungan organisasi");
return true;
} else {
throw new Error(result?.message || "Gagal mengupdate");
}
} catch (error) {
console.error("Update error:", error);
toast.error(error instanceof Error ? error.message : "Gagal update");
return false;
} finally {
this.loading = false;
}
},
reset() {
hubunganOrganisasi.edit.id = "";
hubunganOrganisasi.edit.form = { ...defaultHubunganOrganisasiForm };
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
hubunganOrganisasi.delete.loading = true;
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/del/${id}`,
{
method: "DELETE",
}
);
const result = await res.json();
if (res.ok && result?.success) {
toast.success("Hubungan organisasi berhasil dihapus");
hubunganOrganisasi.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus hubungan organisasi");
}
} catch (error) {
console.error("Delete error:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
hubunganOrganisasi.delete.loading = false;
}
},
},
});
const strukturorganisasiState = proxy({
const stateStrukturBumDes = proxy({
stateStruktur,
posisiOrganisasi,
pegawai,
hubunganOrganisasi,
});
export default strukturorganisasiState;
export default stateStrukturBumDes;

View File

@@ -9,12 +9,14 @@ const templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
whatsapp: z.string().min(10, "Whatsapp minimal 10 karakter"),
});
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
whatsapp: "",
};
const kontakDarurat = proxy({
@@ -171,6 +173,7 @@ const kontakDarurat = proxy({
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
whatsapp: data.whatsapp,
};
return data; // Return the loaded data
} else {
@@ -207,6 +210,7 @@ const kontakDarurat = proxy({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
whatsapp: this.form.whatsapp,
}),
}
);

View File

@@ -388,16 +388,15 @@ const pejabatDesa = proxy({
this.error = null;
try {
const response = await fetch(
`/api/landingpage/pejabatdesa/${this.id}`,
{
// Ensure ID is properly encoded in the URL
const url = new URL(`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`, window.location.origin);
const response = await fetch(url.toString(), {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));

View File

@@ -332,7 +332,7 @@ const keunggulanProgram = proxy({
].post(keunggulanProgram.create.form);
if (res.status === 200) {
keunggulanProgram.findMany.load();
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
return toast.success("Data Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");

View File

@@ -68,6 +68,7 @@ const dataPerpustakaan = proxy({
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
const startTime = Date.now();
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
@@ -77,7 +78,10 @@ const dataPerpustakaan = proxy({
if (search) query.search = search;
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) {
dataPerpustakaan.findMany.data = res.data.data ?? [];
@@ -91,7 +95,52 @@ const dataPerpustakaan = proxy({
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
dataPerpustakaan.findMany.loading = false;
}, delay);
}
},
},
findManyAll: {
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
loading: false,
search: "",
load: async (search = "", kategori = "") => {
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.search = search;
try {
const query: any = {};
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findManyAll"
].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
} else {
dataPerpustakaan.findManyAll.data = [];
}
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findManyAll.data = [];
} finally {
dataPerpustakaan.findManyAll.loading = false;
}
},
},
@@ -330,7 +379,10 @@ const kategoriBuku = proxy({
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? [];
@@ -514,9 +566,319 @@ const kategoriBuku = proxy({
},
});
const templatePeminjamanBuku = z.object({
nama: z.string().min(1, "Nama harus diisi"),
noTelp: z.string().min(1, "No Telp harus diisi"),
alamat: z.string().min(1, "Alamat harus diisi"),
bukuId: z.string().min(1, "Buku ID harus diisi"),
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
catatan: z.string().min(1, "Catatan harus diisi"),
});
const defaultPeminjamanBuku = {
nama: "",
noTelp: "",
alamat: "",
bukuId: "",
tanggalPinjam: "",
batasKembali: "",
tanggalKembali: "",
catatan: "",
};
interface FormEditData {
nama: string;
noTelp: string;
alamat: string;
bukuId: string;
buku?: {
id: string;
judul: string;
};
tanggalPinjam: string;
batasKembali: string;
tanggalKembali: string;
catatan: string;
status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
}
const editForm: FormEditData = {
nama: "",
noTelp: "",
alamat: "",
bukuId: "",
tanggalPinjam: "",
batasKembali: "",
tanggalKembali: "",
catatan: "",
status: "Dipinjam",
};
const peminjamanBuku = proxy({
create: {
form: { ...defaultPeminjamanBuku },
loading: false,
async create() {
const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
peminjamanBuku.create.loading = true;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
"create"
].post(peminjamanBuku.create.form);
if (res.status === 200) {
peminjamanBuku.findMany.load();
return toast.success("Data Peminjaman Buku Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
peminjamanBuku.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.PeminjamanBukuGetPayload<{
include: {
buku: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path
peminjamanBuku.findMany.page = page;
peminjamanBuku.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
peminjamanBuku.findMany.data = res.data.data ?? [];
peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
} else {
peminjamanBuku.findMany.data = [];
peminjamanBuku.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data peminjaman buku paginated:", err);
peminjamanBuku.findMany.data = [];
peminjamanBuku.findMany.totalPages = 1;
} finally {
peminjamanBuku.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PeminjamanBukuGetPayload<{
include: {
buku: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`
);
if (res.ok) {
const data = await res.json();
peminjamanBuku.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
peminjamanBuku.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
peminjamanBuku.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
peminjamanBuku.delete.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/peminjamanbuku/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data Peminjaman Buku berhasil dihapus"
);
await peminjamanBuku.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus Data Peminjaman Buku"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Peminjaman Buku");
} finally {
peminjamanBuku.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...editForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
noTelp: data.noTelp,
alamat: data.alamat,
bukuId: data.bukuId,
tanggalPinjam: data.tanggalPinjam,
batasKembali: data.batasKembali,
tanggalKembali: data.tanggalKembali,
catatan: data.catatan,
status: data.status,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading peminjaman buku:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
peminjamanBuku.update.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
noTelp: this.form.noTelp,
alamat: this.form.alamat,
bukuId: this.form.bukuId,
tanggalPinjam: this.form.tanggalPinjam,
batasKembali: this.form.batasKembali,
tanggalKembali: this.form.tanggalKembali,
catatan: this.form.catatan,
status: this.form.status,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update data peminjaman buku");
await peminjamanBuku.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal update data peminjaman buku"
);
}
} catch (error) {
console.error("Error updating data peminjaman buku:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data peminjaman buku"
);
return false;
} finally {
peminjamanBuku.update.loading = false;
}
},
reset() {
peminjamanBuku.update.id = "";
peminjamanBuku.update.form = { ...editForm };
},
},
});
const perpustakaanDigitalState = proxy({
dataPerpustakaan,
kategoriBuku,
peminjamanBuku,
});
export default perpustakaanDigitalState;

View File

@@ -112,7 +112,32 @@ const statepermohonanInformasiPublik = proxy({
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
}
}
},
findUnique: {
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
include: {
jenisInformasiDiminta: true,
caraMemperolehInformasi: true,
caraMemperolehSalinanInformasi: true,
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
if (res.ok) {
const data = await res.json();
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch program inovasi:", res.statusText);
statepermohonanInformasiPublik.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching program inovasi:", error);
statepermohonanInformasiPublik.findUnique.data = null;
}
},
},
})
const statepermohonanInformasiPublikForm = proxy({

View File

@@ -57,6 +57,28 @@ const permohonanKeberatanInformasi = proxy({
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
}
}
},
findUnique: {
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
if (res.ok) {
const data = await res.json();
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
permohonanKeberatanInformasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching permohonan keberatan informasi:", error);
permohonanKeberatanInformasi.findUnique.data = null;
}
},
}
});

View File

@@ -381,7 +381,44 @@ const posisiOrganisasi = proxy({
}
},
},
findManyAll: {
data: [] as Array<{
id: string;
nama: string;
deskripsi: string | null;
hierarki: number;
}>,
loading: false,
search: "",
load: async (search = "") => {
// Change to arrow function
posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
posisiOrganisasi.findManyAll.search = search;
try {
const query: any = { search };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
"find-many-all"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load posisiOrganisasi:", res.data?.message);
posisiOrganisasi.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading posisiOrganisasi:", error);
posisiOrganisasi.findManyAll.data = [];
} finally {
posisiOrganisasi.findManyAll.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
@@ -524,9 +561,48 @@ const pegawai = proxy({
}
},
},
findManyAll: {
data: null as
| Prisma.PegawaiPPIDGetPayload<{
include: {
image: true;
posisi: true;
};
}>[]
| null,
loading: false,
search: "",
load: async (search = "") => {
// Change to arrow function
pegawai.findManyAll.loading = true; // Use the full path to access the property
pegawai.findManyAll.search = search;
try {
const query: any = { search };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"find-many-all"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
pegawai.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load pegawai:", res.data?.message);
pegawai.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findManyAll.data = [];
} finally {
pegawai.findManyAll.loading = false;
}
},
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
| (Prisma.PegawaiPPIDGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,
@@ -571,6 +647,31 @@ const pegawai = proxy({
},
},
nonActive: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.nonActive.loading = true;
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
method: "DELETE", // biasanya nonActive pakai PATCH
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
await pegawai.findMany.load(); // refresh data
} else {
toast.error(json.message ?? "Gagal menonaktifkan pegawai");
}
} catch (error) {
console.error("Gagal nonActive:", error);
toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
} finally {
pegawai.nonActive.loading = false;
}
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
import {
@@ -10,7 +11,7 @@ import {
Stack,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -24,7 +25,7 @@ function EditKategoriBerita() {
const params = useParams();
const [formData, setFormData] = useState({
name: editState.update.form.name || '',
name: '',
});
useEffect(() => {
@@ -48,8 +49,16 @@ function EditKategoriBerita() {
loadKategori();
}, [params?.id]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSubmit = async () => {
try {
// update global state hanya saat submit
editState.update.form = {
...editState.update.form,
name: formData.name,
@@ -94,10 +103,11 @@ function EditKategoriBerita() {
>
<Stack gap="md">
<TextInput
name="name"
label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={handleChange}
required
/>

View File

@@ -19,7 +19,12 @@ import {
Tooltip,
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import {
IconArrowBack,
IconPhoto,
IconUpload,
IconX,
} from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
@@ -33,16 +38,17 @@ function EditBerita() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
judul: beritaState.berita.edit.form.judul || "",
deskripsi: beritaState.berita.edit.form.deskripsi || "",
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
content: beritaState.berita.edit.form.content || "",
imageId: beritaState.berita.edit.form.imageId || "",
judul: "",
deskripsi: "",
kategoriBeritaId: "",
content: "",
imageId: "",
});
// Load berita by id saat pertama kali
// Load kategori + berita
useEffect(() => {
beritaState.kategoriBerita.findMany.load();
const loadBerita = async () => {
const id = params?.id as string;
if (!id) return;
@@ -71,8 +77,13 @@ function EditBerita() {
loadBerita();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
// Update global state hanya sekali di sini
beritaState.berita.edit.form = {
...beritaState.berita.edit.form,
...formData,
@@ -103,6 +114,7 @@ function EditBerita() {
return (
<Box px={{ base: "sm", md: "lg" }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
@@ -119,6 +131,7 @@ function EditBerita() {
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
@@ -131,18 +144,14 @@ function EditBerita() {
<TextInput
label="Judul"
placeholder="Masukkan judul"
defaultValue={formData.judul}
onChange={(e) =>
setFormData({ ...formData, judul: e.target.value })
}
value={formData.judul}
onChange={(e) => handleChange("judul", e.target.value)}
required
/>
<Select
value={formData.kategoriBeritaId}
onChange={(val) =>
setFormData({ ...formData, kategoriBeritaId: val || "" })
}
onChange={(val) => handleChange("kategoriBeritaId", val || "")}
label="Kategori"
placeholder="Pilih kategori"
data={
@@ -157,16 +166,20 @@ function EditBerita() {
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
defaultValue={formData.deskripsi}
onChange={(e) =>
setFormData({ ...formData, deskripsi: e.target.value })
<Box>
<Text fz="sm" fw="bold">
Deskripsi Singkat
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
required
/>
</Box>
{/* Upload Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Berita
@@ -179,7 +192,9 @@ function EditBerita() {
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
onReject={() =>
toast.error("File tidak valid, gunakan format gambar")
}
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
radius="md"
@@ -187,7 +202,11 @@ function EditBerita() {
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
<IconUpload
size={48}
color={colors["blue-button"]}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
@@ -223,18 +242,20 @@ function EditBerita() {
)}
</Box>
{/* Konten */}
<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;
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, content: htmlContent }))
}
/>
</Box>
{/* Action */}
<Group justify="right">
<Button
onClick={handleSubmit}

View File

@@ -80,7 +80,7 @@ function DetailBerita() {
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.deskripsi || '-'}</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} />
</Box>
<Box>

View File

@@ -131,13 +131,17 @@ export default function CreateBerita() {
required
/>
<TextInput
label="Deskripsi Singkat"
required
placeholder="Masukkan deskripsi berita"
defaultValue={beritaState.berita.create.form.deskripsi}
onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi Singkat
</Text>
<CreateEditor
value={beritaState.berita.create.form.deskripsi}
onChange={(htmlContent) => {
beritaState.berita.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>

View File

@@ -1,31 +1,11 @@
// export function convertYoutubeUrlToEmbed(url: string) {
// const videoIdMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
// return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
// }
export function convertYoutubeUrlToEmbed(url: string) {
const videoIdMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
const videoIdMatch = url.match(
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
);
return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
}
// (url: string): string | null {
// const watchRegex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/;
// const shortRegex = /(?:https?:\/\/)?youtu\.be\/([^?]+)/;
// const matchWatch = url.match(watchRegex);
// const matchShort = url.match(shortRegex);
// if (matchWatch) {
// return `https://www.youtube.com/embed/${matchWatch[1]}`;
// }
// if (matchShort) {
// return `https://www.youtube.com/embed/${matchShort[1]}`;
// }
// return null;
// }

View File

@@ -15,7 +15,7 @@ import {
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
@@ -31,17 +31,19 @@ function EditVideo() {
linkVideo: '',
});
// load data video sekali saat id ada
useEffect(() => {
const loadVideo = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await videoState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
linkVideo: data.linkVideo || '',
name: data.name ?? '',
deskripsi: data.deskripsi ?? '',
linkVideo: data.linkVideo ?? '',
});
}
} catch (error) {
@@ -49,10 +51,16 @@ function EditVideo() {
toast.error('Gagal memuat data video');
}
};
loadVideo();
}, [params?.id]);
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
const handleChange = useCallback(
(field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
@@ -63,7 +71,6 @@ function EditVideo() {
try {
videoState.update.form = {
...videoState.update.form,
name: formData.name,
deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo,
@@ -77,11 +84,18 @@ function EditVideo() {
}
};
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -102,8 +116,8 @@ function EditVideo() {
<TextInput
label="Judul Video"
placeholder="Masukkan judul video"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name', e.currentTarget.value)}
required
/>
@@ -111,8 +125,8 @@ function EditVideo() {
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
defaultValue={formData.linkVideo}
onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })}
value={formData.linkVideo}
onChange={(e) => handleChange('linkVideo', e.currentTarget.value)}
required
/>
{embedLink && (
@@ -135,7 +149,7 @@ function EditVideo() {
</Title>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={(val) => handleChange('deskripsi', val)}
/>
</Box>

View File

@@ -1,5 +1,5 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
@@ -11,7 +11,7 @@ import {
Stack,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -24,16 +24,19 @@ function EditAjukanPermohonan() {
const params = useParams();
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
// State lokal form
const [formData, setFormData] = useState({
nama: stateAjukan.edit.form.nama,
nik: stateAjukan.edit.form.nik,
alamat: stateAjukan.edit.form.alamat,
nomorKk: stateAjukan.edit.form.nomorKk,
kategoriId: stateAjukan.edit.form.kategoriId,
nama: '',
nik: '',
alamat: '',
nomorKk: '',
kategoriId: '',
});
// Load data awal
useEffect(() => {
stateLayananDesa.suratKeterangan.findManyAll.load();
const loadAjukan = async () => {
const id = params?.id as string;
if (!id) return;
@@ -58,6 +61,14 @@ function EditAjukanPermohonan() {
loadAjukan();
}, [params?.id]);
// Handler untuk input controlled
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
stateAjukan.edit.form = {
@@ -98,8 +109,8 @@ function EditAjukanPermohonan() {
<TextInput
label="Nama"
placeholder="Masukkan nama"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
@@ -107,16 +118,16 @@ function EditAjukanPermohonan() {
type="number"
label="NIK"
placeholder="Masukkan NIK"
defaultValue={formData.nik}
onChange={(e) => setFormData({ ...formData, nik: e.target.value })}
value={formData.nik}
onChange={(e) => handleChange('nik', e.target.value)}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
defaultValue={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
value={formData.alamat}
onChange={(e) => handleChange('alamat', e.target.value)}
required
/>
@@ -124,8 +135,8 @@ function EditAjukanPermohonan() {
type="number"
label="Nomor KK"
placeholder="Masukkan nomor KK"
defaultValue={formData.nomorKk}
onChange={(e) => setFormData({ ...formData, nomorKk: e.target.value })}
value={formData.nomorKk}
onChange={(e) => handleChange('nomorKk', e.target.value)}
required
/>
@@ -137,18 +148,7 @@ function EditAjukanPermohonan() {
value: item.id,
}))}
value={formData.kategoriId || null}
onChange={(val: string | null) => {
if (val) {
const selected = stateLayananDesa.suratKeterangan.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
stateAjukan.edit.form.kategoriId = selected.id;
}
} else {
stateAjukan.edit.form.kategoriId = '';
}
}}
onChange={(val) => handleChange('kategoriId', val || '')}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"

View File

@@ -1,9 +1,20 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -12,17 +23,22 @@ import { useProxy } from 'valtio/utils';
function EditPelayananPendudukNonPermanent() {
const router = useRouter();
const params = useParams()
const statePendudukNonPermanent = useProxy(stateLayananDesa.pelayananPendudukNonPermanen)
const [formData, setFormData] = useState({
name: statePendudukNonPermanent.findById.data?.name || '',
deskripsi: statePendudukNonPermanent.findById.data?.deskripsi || '',
})
const params = useParams();
const statePendudukNonPermanent = useProxy(
stateLayananDesa.pelayananPendudukNonPermanen
);
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
});
// Load data sekali dari backend
useEffect(() => {
const loadPelayananPerizinan = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePendudukNonPermanent.update.load(id);
if (data) {
@@ -32,27 +48,48 @@ function EditPelayananPendudukNonPermanent() {
});
}
} catch (error) {
console.error("Error loading pelayanan perizinan berusaha:", error);
toast.error("Gagal memuat data pelayanan perizinan berusaha");
console.error('Error loading data:', error);
toast.error('Gagal memuat data pelayanan penduduk non permanent');
}
};
loadPelayananPerizinan();
loadData();
}, [params?.id]);
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
if (statePendudukNonPermanent.findById.data) {
statePendudukNonPermanent.findById.data.name = formData.name;
statePendudukNonPermanent.findById.data.deskripsi = formData.deskripsi;
statePendudukNonPermanent.update.update(statePendudukNonPermanent.findById.data)
}
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent')
}
if (!statePendudukNonPermanent.findById.data) return;
// Update global state hanya di submit
const updated = {
...statePendudukNonPermanent.findById.data,
name: formData.name,
deskripsi: formData.deskripsi,
};
await statePendudukNonPermanent.update.update(updated);
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
};
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -62,7 +99,7 @@ function EditPelayananPendudukNonPermanent() {
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="md"
radius="md"
@@ -76,23 +113,19 @@ function EditPelayananPendudukNonPermanent() {
<TextInput
label="Judul"
placeholder="Masukkan judul"
defaultValue={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
value={formData.name}
onChange={(e) => handleChange('name')(e.target.value)}
required
/>
{/* Posisi Field */}
{/* Deskripsi Field */}
<Box>
<Text fz="sm" fw="bold">
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
}}
onChange={handleChange('deskripsi')}
/>
</Box>
@@ -104,7 +137,9 @@ function EditPelayananPendudukNonPermanent() {
loading={statePendudukNonPermanent.update.loading}
disabled={!formData.name}
>
{statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
{statePendudukNonPermanent.update.loading
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button>
<Button

View File

@@ -27,7 +27,7 @@ function PelayananPendudukNonPermanent() {
);
useShallowEffect(() => {
pelayananPendudukNonPermanen.findById.load('1');
pelayananPendudukNonPermanen.findById.load('edit');
}, []);
if (!pelayananPendudukNonPermanen.findById.data) {
@@ -59,7 +59,7 @@ function PelayananPendudukNonPermanent() {
radius="md"
onClick={() =>
router.push(
'/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit'
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
)
}
>

View File

@@ -1,9 +1,20 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -12,53 +23,90 @@ import { useProxy } from 'valtio/utils';
function EditPelayananPerizinanBerusaha() {
const router = useRouter();
const params = useParams()
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
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({
name: statePerizinanBerusaha.findById.data?.name || '',
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
link: statePerizinanBerusaha.findById.data?.link || '',
})
id: '',
name: '',
deskripsi: '',
link: '',
});
// Load data detail
useEffect(() => {
const loadPelayananPerizinan = async () => {
const id = params?.id as string;
if (!id) return;
if (!id) {
toast.error("ID tidak valid");
return;
}
const loadData = async () => {
try {
const data = await statePerizinanBerusaha.update.load(id);
setLoading(true);
const data = await state.findById.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
link: data.link || '',
id: data.id,
name: data.name || "",
deskripsi: data.deskripsi || "",
link: data.link || "",
});
} else {
toast.error("Data tidak ditemukan");
}
} catch (error) {
console.error("Error loading pelayanan perizinan berusaha:", error);
toast.error("Gagal memuat data pelayanan perizinan berusaha");
console.error("Error loading data:", error);
toast.error("Gagal memuat data");
} finally {
setLoading(false);
}
};
loadPelayananPerizinan();
}, [params?.id]);
loadData();
}, [id]);
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
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)
try {
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');
}
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
};
if (loading) {
return (
<Stack align="center" justify="center" py="xl">
<Skeleton height={800} radius="md" />
</Stack>
);
}
return (
<Box>
<Stack gap="xs">
{/* Header Section */}
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -67,9 +115,9 @@ function EditPelayananPerizinanBerusaha() {
</Title>
</Group>
{/* Form Section */}
{/* Form */}
<Paper
w={{ base: "100%", md: "50%" }}
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="md"
radius="md"
@@ -79,47 +127,43 @@ function EditPelayananPerizinanBerusaha() {
<Stack gap="xs">
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
{/* Nama Field */}
<TextInput
label="Judul"
placeholder="Masukkan judul"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name')(e.target.value)}
required
/>
{/* Link Field */}
<TextInput
label="Link"
placeholder="Masukkan link terkait"
defaultValue={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
value={formData.link}
onChange={(e) => handleChange('link')(e.target.value)}
/>
{/* Deskripsi Field */}
<Box>
<Title order={6}>Deskripsi</Title>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={handleChange('deskripsi')}
/>
</Box>
{/* Action Buttons */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePerizinanBerusaha.update.loading}
loading={state.update.loading}
disabled={!formData.name}
>
{statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
{state.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={() => router.back()}
disabled={statePerizinanBerusaha.update.loading}
disabled={state.update.loading}
>
Batal
</Button>

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import {
@@ -19,35 +20,58 @@ import {
Tooltip,
} from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
function PerizinanBerusaha() {
const router = useRouter();
const pelayananPerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [active, setActive] = useState(1);
const nextStep = () =>
setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () =>
setActive((current) => (current > 0 ? current - 1 : current));
useShallowEffect(() => {
pelayananPerizinanBerusaha.findById.load('1');
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
// You should get the ID from your router query or params
const id = 'edit'; // Replace with actual ID or get from URL params
await pelayananPerizinanBerusaha.findById.load(id);
} catch (err) {
setError('Gagal memuat data');
console.error('Error:', err);
} finally {
setLoading(false);
}
};
loadData();
}, []);
if (!pelayananPerizinanBerusaha.findById.data) {
if (loading) {
return (
<Stack align="center" justify="center" py="xl">
<Skeleton radius="md" height={800} />
<Skeleton height={800} radius="md" />
</Stack>
);
}
if (error || !pelayananPerizinanBerusaha.findById.data) {
return (
<Center h={200}>
<Text>{error || 'Data tidak ditemukan'}</Text>
</Center>
);
}
const data = pelayananPerizinanBerusaha.findById.data;
return (
@@ -69,7 +93,7 @@ function PerizinanBerusaha() {
radius="md"
onClick={() =>
router.push(
'/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
)
}
>
@@ -183,3 +207,4 @@ function PerizinanBerusaha() {
}
export default PerizinanBerusaha;

View File

@@ -1,5 +1,4 @@
'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';
@@ -19,7 +18,7 @@ import {
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
@@ -28,17 +27,23 @@ function EditSuratKeterangan() {
const params = useParams();
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
// state lokal untuk form
const [formData, setFormData] = useState({
name: stateSurat.edit.form.name,
deskripsi: stateSurat.edit.form.deskripsi,
imageId: stateSurat.edit.form.imageId,
image2Id: stateSurat.edit.form.image2Id,
name: '',
deskripsi: '',
imageId: '',
image2Id: '',
});
// state file upload
const [file, setFile] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
// state preview gambar
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
// load data awal
useEffect(() => {
const loadSurat = async () => {
const id = params?.id as string;
@@ -46,33 +51,39 @@ function EditSuratKeterangan() {
try {
const data = await stateSurat.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
image2Id: data.image2Id || '',
});
if (!data) return;
setPreviewImage(data.image?.link || null);
setPreviewImage2(data.image2?.link || null);
}
setFormData((prev) => ({
...prev,
...{
name: prev.name || data.name || "",
deskripsi: prev.deskripsi || data.deskripsi || "",
imageId: prev.imageId || data.imageId || "",
image2Id: prev.image2Id || data.image2Id || "",
},
}));
if (data.image?.link && !previewImage) setPreviewImage(data.image.link);
if (data.image2?.link && !previewImage2) setPreviewImage2(data.image2.link);
} catch (error) {
console.error('Error loading surat:', error);
toast.error('Gagal memuat data surat');
console.error("Error loading surat:", error);
toast.error("Gagal memuat data surat");
}
};
loadSurat();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.id]);
const handleSubmit = async () => {
try {
stateSurat.edit.form = {
...stateSurat.edit.form,
...formData,
};
// handler untuk submit
const handleSubmit = useCallback(async () => {
try {
// update form global hanya saat submit
stateSurat.edit.form = { ...stateSurat.edit.form, ...formData };
// upload file 1
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
@@ -80,6 +91,7 @@ function EditSuratKeterangan() {
stateSurat.edit.form.imageId = uploaded.id;
}
// upload file 2
if (file2) {
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
const uploaded = res.data?.data;
@@ -94,7 +106,7 @@ function EditSuratKeterangan() {
console.error('Error updating surat:', error);
toast.error('Terjadi kesalahan saat memperbarui surat');
}
};
}, [formData, file, file2, router, stateSurat.edit]);
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -119,21 +131,25 @@ function EditSuratKeterangan() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input nama */}
<TextInput
label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required
/>
{/* Input deskripsi */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>

View File

@@ -8,17 +8,15 @@ import {
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
@@ -26,22 +24,24 @@ function EditPelayananTelunjukSakti() {
const params = useParams();
const [formData, setFormData] = useState({
name: stateTelunjukDesa.edit.form.name,
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
link: stateTelunjukDesa.edit.form.link,
name: '',
deskripsi: '',
link: '',
});
// Load data awal hanya sekali (pas ada id)
useEffect(() => {
const loadPelayananTelunjukSakti = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateTelunjukDesa.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
link: data.link || '',
name: data.name ?? '',
deskripsi: data.deskripsi ?? '',
link: data.link ?? '',
});
}
} catch (error) {
@@ -49,9 +49,19 @@ function EditPelayananTelunjukSakti() {
toast.error('Gagal memuat data pelayanan telunjuk sakti');
}
};
loadPelayananTelunjukSakti();
loadData();
}, [params?.id]);
// Handler input controlled
const handleChange = useCallback(
(field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
// Submit: update global state hanya saat simpan
const handleSubmit = async () => {
try {
stateTelunjukDesa.edit.form = {
@@ -94,28 +104,26 @@ function EditPelayananTelunjukSakti() {
<TextInput
label="Nama Pelayanan"
placeholder="Masukkan nama pelayanan"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
required
/>
{/* Deskripsi pakai editor */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor
{/* Deskripsi */}
<TextInput
value={formData.deskripsi}
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
onChange={(e) => handleChange('deskripsi', e.target.value)}
label="Judul Link"
placeholder="Masukkan judul link"
required
/>
</Box>
{/* Link */}
<TextInput
label="Link"
placeholder="Masukkan link terkait"
defaultValue={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
placeholder="Masukkan alamat link"
value={formData.link}
onChange={(e) => handleChange('link', e.target.value)}
/>
{/* Tombol Simpan */}

View File

@@ -82,8 +82,8 @@ function CreatePelayananTelunjukDesa() {
onChange={(val) => {
stateTelunjukDesa.create.form.deskripsi = val.target.value;
}}
label="Deskripsi"
placeholder="Masukkan deskripsi pelayanan"
label="Judul Link"
placeholder="Masukkan judul link"
required
/>

View File

@@ -24,18 +24,22 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPenghargaan() {
const statePenghargaan = useProxy(penghargaanState)
const router = useRouter()
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
const [formData, setFormData] = useState({
name: statePenghargaan.findUnique.data?.name || '',
juara: statePenghargaan.findUnique.data?.juara || '',
deskripsi: statePenghargaan.findUnique.data?.deskripsi || '',
imageId: statePenghargaan.findUnique.data?.imageId || '',
})
const statePenghargaan = useProxy(penghargaanState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// Lokal formData
const [formData, setFormData] = useState({
name: '',
juara: '',
deskripsi: '',
imageId: '',
});
// Load data pertama kali
useEffect(() => {
const loadPenghargaan = async () => {
const id = params?.id as string;
@@ -56,43 +60,43 @@ function EditPenghargaan() {
}
}
} catch (error) {
console.error("Error loading penghargaan:", error);
toast.error("Gagal memuat data penghargaan");
console.error('Error loading penghargaan:', error);
toast.error('Gagal memuat data penghargaan');
}
};
loadPenghargaan();
}, [params?.id]);
// Submit
const handleSubmit = async () => {
try {
// Sync ke global state saat submit
statePenghargaan.edit.form = {
...statePenghargaan.edit.form,
name: formData.name,
juara: formData.juara,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
...formData,
};
// Upload file baru (kalau ada)
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
return toast.error('Gagal upload gambar');
}
statePenghargaan.edit.form.imageId = uploaded.id;
}
await statePenghargaan.edit.update();
toast.success("Penghargaan berhasil diperbarui!");
router.push("/admin/desa/penghargaan");
toast.success('Penghargaan berhasil diperbarui!');
router.push('/admin/desa/penghargaan');
} catch (error) {
console.error("Error updating penghargaan:", error);
toast.error("Terjadi kesalahan saat memperbarui penghargaan");
}
console.error('Error updating penghargaan:', error);
toast.error('Terjadi kesalahan saat memperbarui penghargaan');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -122,8 +126,8 @@ function EditPenghargaan() {
<TextInput
label="Judul"
placeholder="Masukkan judul penghargaan"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required
/>
@@ -131,8 +135,8 @@ function EditPenghargaan() {
<TextInput
label="Juara"
placeholder="Masukkan juara"
defaultValue={formData.juara}
onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
value={formData.juara}
onChange={(e) => setFormData((prev) => ({ ...prev, juara: e.target.value }))}
required
/>
@@ -182,7 +186,11 @@ function EditPenghargaan() {
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
@@ -196,10 +204,9 @@ function EditPenghargaan() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
statePenghargaan.edit.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import {
@@ -10,7 +11,7 @@ import {
Stack,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -23,10 +24,9 @@ function EditKategoriPengumuman() {
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: editState.update.form.name || '',
});
const [formData, setFormData] = useState({ name: '' });
// Load data awal sekali aja
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
@@ -35,9 +35,7 @@ function EditKategoriPengumuman() {
try {
const data = await editState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
});
setFormData({ name: data.name || '' });
}
} catch (error) {
console.error('Error loading kategori Pengumuman:', error);
@@ -48,8 +46,16 @@ function EditKategoriPengumuman() {
loadKategori();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// Update global state hanya di sini
editState.update.form = {
...editState.update.form,
name: formData.name,
@@ -97,9 +103,7 @@ function EditKategoriPengumuman() {
label="Nama Kategori Pengumuman"
placeholder="Masukkan nama kategori Pengumuman"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
onChange={(e) => handleChange('name', e.target.value)}
required
/>

View File

@@ -28,16 +28,16 @@ function EditPengumuman() {
const params = useParams();
const [formData, setFormData] = useState({
judul: editState.pengumuman.edit.form.judul || "",
deskripsi: editState.pengumuman.edit.form.deskripsi || "",
categoryPengumumanId:
editState.pengumuman.edit.form.categoryPengumumanId || "",
content: editState.pengumuman.edit.form.content || "",
judul: "",
deskripsi: "",
categoryPengumumanId: "",
content: "",
});
// Load pengumuman by id saat pertama kali
// Load kategori & pengumuman by id saat pertama kali
useEffect(() => {
editState.category.findMany.load();
const loadpengumuman = async () => {
const id = params?.id as string;
if (!id) return;
@@ -61,9 +61,13 @@ function EditPengumuman() {
loadpengumuman();
}, [params?.id]);
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
// update global state
// update global state hanya sekali pas submit
editState.pengumuman.edit.form = {
...editState.pengumuman.edit.form,
...formData,
@@ -108,26 +112,22 @@ function EditPengumuman() {
<TextInput
label="Judul Pengumuman"
placeholder="Masukkan judul"
defaultValue={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
value={formData.judul}
onChange={(e) => handleChange("judul", e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
defaultValue={formData.deskripsi}
onChange={(e) =>
setFormData({ ...formData, deskripsi: e.target.value })
}
value={formData.deskripsi}
onChange={(e) => handleChange("deskripsi", e.target.value)}
required
/>
<Select
value={formData.categoryPengumumanId}
onChange={(val) =>
setFormData({ ...formData, categoryPengumumanId: val || "" })
}
onChange={(val) => handleChange("categoryPengumumanId", val || "")}
label="Kategori"
placeholder="Pilih kategori"
data={
@@ -150,9 +150,7 @@ function EditPengumuman() {
</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) =>
setFormData({ ...formData, content: htmlContent })
}
onChange={(htmlContent) => handleChange("content", htmlContent)}
/>
</Box>

View File

@@ -24,9 +24,10 @@ function EditKategoriPotensi() {
const params = useParams();
const [formData, setFormData] = useState({
nama: editState.update.form.nama || '',
nama: '',
});
// Load data dari backend -> isi ke formData lokal
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
@@ -48,8 +49,16 @@ function EditKategoriPotensi() {
loadKategori();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// Update global state hanya pas submit
editState.update.form = {
...editState.update.form,
nama: formData.nama,
@@ -68,7 +77,12 @@ function EditKategoriPotensi() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -89,12 +103,12 @@ function EditKategoriPotensi() {
<TextInput
label="Nama Kategori Potensi"
placeholder="Masukkan nama kategori potensi"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.currentTarget.value)}
required
/>
<Group justify="right">
<Group justify="flex-end">
<Button
onClick={handleSubmit}
radius="md"

View File

@@ -40,8 +40,14 @@ function EditPotensi() {
imageId: "",
});
// handle input changes
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load();
const loadPotensi = async () => {
const id = params?.id as string;
if (!id) return;
@@ -57,6 +63,16 @@ function EditPotensi() {
imageId: data.imageId || "",
});
// // merge, bukan replace
// setFormData((prev) => ({
// ...prev,
// name: data.name ?? prev.name,
// deskripsi: data.deskripsi ?? prev.deskripsi,
// kategoriId: data.kategoriId ?? prev.kategoriId,
// content: data.content ?? prev.content,
// imageId: data.imageId ?? prev.imageId,
// }));
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
@@ -70,24 +86,30 @@ function EditPotensi() {
loadPotensi();
}, [params?.id]);
const handleSubmit = async () => {
try {
potensiState.edit.form = {
...potensiState.edit.form,
...formData,
};
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
potensiState.edit.form.imageId = uploaded.id;
imageId = uploaded.id;
}
potensiState.edit.form = {
...formData,
imageId,
};
await potensiState.edit.update();
toast.success("Potensi berhasil diperbarui!");
router.push("/admin/desa/potensi/list-potensi");
@@ -101,7 +123,12 @@ function EditPotensi() {
<Box px={{ base: "sm", md: "lg" }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
</Tooltip>
@@ -122,22 +149,26 @@ function EditPotensi() {
<TextInput
label="Judul Potensi"
placeholder="Masukkan judul"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
defaultValue={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
required
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi Singkat
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
handleChange("deskripsi", htmlContent)
}
/>
</Box>
<Select
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
onChange={(val) => handleChange("kategoriId", val || "")}
label="Kategori"
placeholder="Pilih kategori"
data={
@@ -164,7 +195,9 @@ function EditPotensi() {
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
onReject={() =>
toast.error("File tidak valid, gunakan format gambar")
}
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
radius="md"
@@ -172,7 +205,11 @@ function EditPotensi() {
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
<IconUpload
size={48}
color={colors["blue-button"]}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
@@ -214,7 +251,9 @@ function EditPotensi() {
</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) => setFormData({ ...formData, content: htmlContent })}
onChange={(htmlContent) =>
handleChange("content", htmlContent)
}
/>
</Box>

View File

@@ -25,7 +25,7 @@ export default function DetailPotensi() {
potensiState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/desa/potensi");
router.push("/admin/desa/potensi/list-potensi");
}
};
@@ -77,7 +77,7 @@ export default function DetailPotensi() {
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.deskripsi || '-'}</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}></Text>
</Box>
<Box>

View File

@@ -101,13 +101,17 @@ function CreatePotensi() {
/>
{/* Deskripsi */}
<TextInput
defaultValue={potensiState.create.form.deskripsi}
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
label="Deskripsi"
placeholder="Masukkan deskripsi singkat"
required
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi Singkat
</Text>
<CreateEditor
value={potensiState.create.form.deskripsi}
onChange={(htmlContent) => {
potensiState.create.form.deskripsi = htmlContent;
}}
/>
</Box>
{/* Kategori */}
<Select

View File

@@ -102,20 +102,25 @@ function ListPotensi({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text fz="sm" c="dimmed">{item.kategori?.nama || '-'}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={300}>
<Text
lineClamp={1}
truncate
fz="sm"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
</TableTd>

View File

@@ -18,7 +18,9 @@ function Page() {
const router = useRouter();
const params = useParams();
const [images, setImages] = useState<Array<{ file: File | null; preview: string; label: string }>>([]);
const [images, setImages] = useState<
Array<{ file: File | null; preview: string; label: string; imageId?: string }>
>([]);
const [formData, setFormData] = useState({
judul: '',
deskripsi: '',
@@ -55,6 +57,7 @@ function Page() {
file: null,
preview: img.image.link,
label: img.label,
imageId: img.image.id, // simpan id lama
})));
}
}
@@ -88,26 +91,25 @@ function Page() {
// Upload semua gambar baru
for (const img of images) {
if (!img.file) {
// Kalau gambar lama, skip upload
if (!img.preview) continue;
uploadedImages.push({ imageId: '', label: img.label });
if (!img.imageId) continue; // kalau benar2 kosong, skip
uploadedImages.push({ imageId: img.imageId, label: img.label });
continue;
}
// upload baru
const res = await ApiFetch.api.fileStorage.create.post({
file: img.file,
name: img.file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload salah satu gambar");
return;
}
uploadedImages.push({ imageId: uploaded.id, label: img.label || 'main' });
uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" });
}
// Update ke global state
maskotState.update.updateField("judul", formData.judul);
maskotState.update.updateField("deskripsi", formData.deskripsi);

View File

@@ -162,7 +162,7 @@ function Page() {
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
Lambang Desa
{lambang.judul}
</Text>
</Paper>
</Stack>
@@ -216,7 +216,7 @@ function Page() {
<Divider my="md" color={colors['blue-button']} />
<Text fz={{ base: "md", md: "h3" }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
<Stack mt="md" gap="sm">
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="md">
<SimpleGrid cols={{ base: 1, md: 4 }} spacing="md">
{maskot.images.map((img, idx) => (
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
<Center>

View File

@@ -26,15 +26,17 @@ function EditPerbekelDariMasaKeMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: state.update.form.nama || '',
daerah: state.update.form.daerah || '',
periode: state.update.form.periode || '',
imageId: state.update.form.imageId || ''
nama: '',
daerah: '',
periode: '',
imageId: ''
});
// load data pertama kali
useEffect(() => {
const loadFoto = async () => {
const id = params?.id as string;
@@ -48,7 +50,9 @@ function EditPerbekelDariMasaKeMasa() {
periode: data.periode || '',
imageId: data.imageId || ''
});
if (data?.imageGalleryFoto?.link) setPreviewImage(data.imageGalleryFoto.link);
if (data?.imageGalleryFoto?.link) {
setPreviewImage(data.imageGalleryFoto.link);
}
}
} catch (error) {
console.error('Error loading foto:', error);
@@ -58,8 +62,17 @@ function EditPerbekelDariMasaKeMasa() {
loadFoto();
}, [params?.id]);
// helper ubah state formData
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// update global state hanya sekali pas submit
state.update.form = { ...state.update.form, ...formData };
if (file) {
@@ -106,8 +119,8 @@ function EditPerbekelDariMasaKeMasa() {
<TextInput
label="Nama"
placeholder="Masukkan nama"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
@@ -161,7 +174,7 @@ function EditPerbekelDariMasaKeMasa() {
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading='lazy'
loading="lazy"
/>
</Box>
)}
@@ -170,16 +183,16 @@ function EditPerbekelDariMasaKeMasa() {
<TextInput
label="Daerah"
placeholder="Masukkan daerah"
defaultValue={formData.daerah}
onChange={(e) => setFormData({ ...formData, daerah: e.target.value })}
value={formData.daerah}
onChange={(e) => handleChange('daerah', e.target.value)}
required
/>
<TextInput
label="Periode"
placeholder="Masukkan periode"
defaultValue={formData.periode}
onChange={(e) => setFormData({ ...formData, periode: e.target.value })}
value={formData.periode}
onChange={(e) => handleChange('periode', e.target.value)}
required
/>

View File

@@ -75,12 +75,17 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} lineClamp={1}>{item.nama}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text lineClamp={1}>{item.periode}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Button
size="xs"
radius="md"
@@ -91,6 +96,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
>
Detail
</Button>
</Box>
</TableTd>
</TableTr>
))

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import {
@@ -29,13 +29,13 @@ function EditAPBDesa() {
const params = useParams();
const [formData, setFormData] = useState({
tahun: apbState.update.form.tahun || '',
pendapatanIds: apbState.update.form.pendapatanIds || [],
belanjaIds: apbState.update.form.belanjaIds || [],
pembiayaanIds: apbState.update.form.pembiayaanIds || [],
tahun: '',
pendapatanIds: [] as string[],
belanjaIds: [] as string[],
pembiayaanIds: [] as string[],
});
// Load APB desa by id
// Load APB desa by id → hanya update formData, bukan global state
useEffect(() => {
const loadAPBdesa = async () => {
const id = params?.id as string;
@@ -45,7 +45,7 @@ function EditAPBDesa() {
const data = await apbState.update.load(id);
if (data) {
setFormData({
tahun: data.tahun || 0,
tahun: String(data.tahun || ''),
pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [],
belanjaIds: data.belanja?.map((b: any) => b.id) || [],
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
@@ -60,8 +60,13 @@ function EditAPBDesa() {
loadAPBdesa();
}, [params?.id]);
const handleChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
// update global state cuma pas submit
apbState.update.form = {
...apbState.update.form,
tahun: Number(formData.tahun),
@@ -111,10 +116,8 @@ function EditAPBDesa() {
{/* Tahun */}
<TextInput
type="number"
defaultValue={formData.tahun}
onChange={(e) =>
setFormData({ ...formData, tahun: e.target.value })
}
value={formData.tahun}
onChange={(e) => handleChange("tahun", e.target.value)}
label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="Masukkan tahun anggaran"
required
@@ -123,23 +126,17 @@ function EditAPBDesa() {
{/* Selects */}
<SelectPendapatan
selectedIds={formData.pendapatanIds}
onSelectionChange={(ids) =>
setFormData({ ...formData, pendapatanIds: ids })
}
onSelectionChange={(ids) => handleChange("pendapatanIds", ids)}
/>
<SelectBelanja
selectedIds={formData.belanjaIds}
onSelectionChange={(ids) =>
setFormData({ ...formData, belanjaIds: ids })
}
onSelectionChange={(ids) => handleChange("belanjaIds", ids)}
/>
<SelectPembiayaan
selectedIds={formData.pembiayaanIds}
onSelectionChange={(ids) =>
setFormData({ ...formData, pembiayaanIds: ids })
}
onSelectionChange={(ids) => handleChange("pembiayaanIds", ids)}
/>
{/* Save Button */}
@@ -164,7 +161,13 @@ function EditAPBDesa() {
/* --- Sub Components --- */
function SelectPendapatan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
function SelectPendapatan({
selectedIds,
onSelectionChange,
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => {
@@ -192,7 +195,13 @@ function EditAPBDesa() {
);
}
function SelectBelanja({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
function SelectBelanja({
selectedIds,
onSelectionChange,
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => {
@@ -220,7 +229,13 @@ function EditAPBDesa() {
);
}
function SelectPembiayaan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
function SelectPembiayaan({
selectedIds,
onSelectionChange,
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => {

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import {
@@ -24,12 +25,16 @@ function EditBelanja() {
const params = useParams();
const [formData, setFormData] = useState({
name: belanjaState.update.form.name || '',
value: belanjaState.update.form.value || '',
name: '',
value: '',
});
// format angka ke rupiah
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
const number =
typeof value === 'number'
? value
: Number(value.replace(/\D/g, '')) || 0;
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -37,8 +42,9 @@ function EditBelanja() {
}).format(number);
};
// buang semua simbol jadi angka murni
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
return Number(value.replace(/\D/g, '')) || 0;
};
useEffect(() => {
@@ -51,7 +57,7 @@ function EditBelanja() {
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
value: String(data.value || ''),
});
}
} catch (error) {
@@ -69,7 +75,7 @@ function EditBelanja() {
...belanjaState.update.form,
name: formData.name,
value: Number(formData.value),
}
};
await belanjaState.update.update();
toast.success("Jenis Belanja berhasil diperbarui!");
@@ -78,7 +84,7 @@ function EditBelanja() {
console.error("Error updating jenis belanja:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis belanja");
}
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -112,19 +118,21 @@ function EditBelanja() {
<TextInput
label="Nama Jenis Belanja"
placeholder="Masukkan nama jenis belanja"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
defaultValue={formatRupiah(formData.value)}
value={formData.value ? formatRupiah(formData.value) : ''}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
setFormData({ ...formData, value: String(cleanValue) });
}}
required
/>

View File

@@ -24,12 +24,15 @@ function EditPembiayaan() {
const params = useParams();
const [formData, setFormData] = useState({
name: pembiayaanState.update.form.name || '',
value: pembiayaanState.update.form.value || '',
name: '',
value: '',
});
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
const number =
typeof value === 'number'
? value
: Number(value.toString().replace(/\D/g, '')) || 0;
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -38,7 +41,7 @@ function EditPembiayaan() {
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
return Number(value.replace(/\D/g, '')) || 0;
};
useEffect(() => {
@@ -51,7 +54,7 @@ function EditPembiayaan() {
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
value: String(data.value || ''),
});
}
} catch (error) {
@@ -68,7 +71,7 @@ function EditPembiayaan() {
pembiayaanState.update.form = {
...pembiayaanState.update.form,
name: formData.name,
value: Number(formData.value),
value: unformatRupiah(formData.value),
};
await pembiayaanState.update.update();
@@ -82,7 +85,7 @@ function EditPembiayaan() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan Back Button */}
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
@@ -112,19 +115,21 @@ function EditPembiayaan() {
<TextInput
label="Nama Jenis Pembiayaan"
placeholder="Masukkan nama jenis pembiayaan"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
defaultValue={formatRupiah(formData.value)}
value={formData.value ? formatRupiah(formData.value) : ''}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
setFormData((prev) => ({ ...prev, value: String(cleanValue) }));
}}
required
/>

View File

@@ -24,12 +24,16 @@ function EditPendapatan() {
const params = useParams();
const [formData, setFormData] = useState({
name: pendapatanState.update.form.name || '',
value: pendapatanState.update.form.value || '',
name: '',
value: '',
});
// helper format
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
const number = typeof value === 'number'
? value
: Number(value.toString().replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -39,6 +43,7 @@ function EditPendapatan() {
const unformatRupiah = (value: string) => Number(value.replace(/\D/g, ''));
// load data once
useEffect(() => {
const id = params?.id as string;
if (!id) return;
@@ -48,8 +53,8 @@ function EditPendapatan() {
const data = await pendapatanState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
name: data.name ?? '',
value: data.value?.toString() ?? '',
});
}
} catch (error) {
@@ -61,6 +66,13 @@ function EditPendapatan() {
loadPendapatan();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
pendapatanState.update.form = {
@@ -110,19 +122,19 @@ function EditPendapatan() {
<TextInput
label="Nama Jenis Pendapatan"
placeholder="Masukkan nama jenis pendapatan"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
defaultValue={formatRupiah(formData.value)}
value={formData.value ? formatRupiah(formData.value) : ''}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
const cleanValue = unformatRupiah(raw).toString();
handleChange('value', cleanValue);
}}
required
/>

View File

@@ -10,47 +10,80 @@ import {
Stack,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function EditDemografiPekerjaan() {
interface FormData {
pekerjaan: string;
lakiLaki: number;
perempuan: number;
}
export default function EditDemografiPekerjaan() {
const router = useRouter();
const params = useParams() as { id: string };
const { id } = useParams() as { id: string };
const stateDemografi = useProxy(demografiPekerjaan);
const id = params.id;
const [formData, setFormData] = useState<FormData>({
pekerjaan: '',
lakiLaki: 0,
perempuan: 0,
});
// ✅ Load data hanya sekali di awal (tidak reset form)
useEffect(() => {
if (!id) return;
const loadData = async () => {
try {
stateDemografi.update.id = id;
stateDemografi.findUnique
.load(id)
.then(() => {
await stateDemografi.findUnique.load(id);
const data = stateDemografi.findUnique.data;
if (data) {
stateDemografi.update.form = {
pekerjaan: String(data.pekerjaan || ''),
lakiLaki: Number(data.lakiLaki || 0),
perempuan: Number(data.perempuan || 0),
};
setFormData({
pekerjaan: data.pekerjaan ?? '',
lakiLaki: Number(data.lakiLaki ?? 0),
perempuan: Number(data.perempuan ?? 0),
});
}
})
.catch((error) => {
} catch (error) {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
});
}
};
loadData();
}, [id]);
// ✅ Handler input terkontrol (tidak buat re-render berlebihan)
const handleChange = useCallback(
(field: keyof FormData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const value =
field === 'lakiLaki' || field === 'perempuan'
? Number(e.currentTarget.value)
: e.currentTarget.value;
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
// ✅ Submit hanya update global state sekali
const handleSubmit = async () => {
try {
stateDemografi.update.id = id;
stateDemografi.update.form = { ...formData };
await stateDemografi.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/ekonomi/demografi-pekerjaan');
} catch (error) {
@@ -91,10 +124,8 @@ function EditDemografiPekerjaan() {
<TextInput
label="Pekerjaan"
placeholder="Masukkan jenis pekerjaan"
defaultValue={stateDemografi.update.form.pekerjaan}
onChange={(e) =>
(stateDemografi.update.form.pekerjaan = e.currentTarget.value)
}
value={formData.pekerjaan}
onChange={handleChange('pekerjaan')}
required
/>
@@ -102,12 +133,8 @@ function EditDemografiPekerjaan() {
label="Jumlah Pekerja Laki-laki"
type="number"
placeholder="Masukkan jumlah pekerja laki-laki"
defaultValue={stateDemografi.update.form.lakiLaki}
onChange={(e) =>
(stateDemografi.update.form.lakiLaki = Number(
e.currentTarget.value
))
}
value={formData.lakiLaki}
onChange={handleChange('lakiLaki')}
required
/>
@@ -115,12 +142,8 @@ function EditDemografiPekerjaan() {
label="Jumlah Pekerja Perempuan"
type="number"
placeholder="Masukkan jumlah pekerja perempuan"
defaultValue={stateDemografi.update.form.perempuan}
onChange={(e) =>
(stateDemografi.update.form.perempuan = Number(
e.currentTarget.value
))
}
value={formData.perempuan}
onChange={handleChange('perempuan')}
required
/>
@@ -143,5 +166,3 @@ function EditDemografiPekerjaan() {
</Box>
);
}
export default EditDemografiPekerjaan;

View File

@@ -126,9 +126,9 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Pekerjaan</TableTh>
<TableTh>Laki - Laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh style={{ minWidth: 200 }}>Pekerjaan</TableTh>
<TableTh style={{ minWidth: 200 }}>Laki - Laki</TableTh>
<TableTh style={{ minWidth: 200 }}>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
@@ -137,9 +137,9 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.pekerjaan}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.lakiLaki}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.perempuan}</TableTd>
<TableTd>
<Button
variant="light"

View File

@@ -3,10 +3,19 @@
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
import {
Box,
Button,
Paper,
Stack,
TextInput,
Title,
Group,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
@@ -17,6 +26,13 @@ function EditJumlahPendudukMiskin() {
const id = params.id;
// 🔹 State lokal untuk form
const [formData, setFormData] = useState({
year: 0,
totalPoorPopulation: 0,
});
// 🔹 Load data awal dari backend
useEffect(() => {
if (!id) return;
@@ -25,10 +41,10 @@ function EditJumlahPendudukMiskin() {
await stateJPM.findUnique.load(id);
const data = stateJPM.findUnique.data;
if (data) {
stateJPM.update.form = {
setFormData({
year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0,
};
});
}
} catch (error) {
console.error('Gagal memuat data:', error);
@@ -39,9 +55,21 @@ function EditJumlahPendudukMiskin() {
loadData();
}, [id]);
// 🔹 Handler input controlled
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: Number(value),
}));
};
// 🔹 Submit form
const handleSubmit = async () => {
try {
stateJPM.update.id = id;
// update global state cuma saat submit
stateJPM.update.form = { ...formData };
await stateJPM.update.submit();
toast.success('Data jumlah penduduk miskin berhasil diperbarui!');
router.push('/admin/ekonomi/jumlah-penduduk-miskin');
@@ -55,7 +83,12 @@ function EditJumlahPendudukMiskin() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -78,10 +111,8 @@ function EditJumlahPendudukMiskin() {
placeholder="Masukkan tahun"
type="number"
required
defaultValue={stateJPM.update.form.year}
onChange={(val) => {
stateJPM.update.form.year = Number(val.currentTarget.value);
}}
value={formData.year}
onChange={(e) => handleChange('year', e.currentTarget.value)}
/>
<TextInput
@@ -89,10 +120,10 @@ function EditJumlahPendudukMiskin() {
placeholder="Masukkan jumlah penduduk miskin"
type="number"
required
defaultValue={stateJPM.update.form.totalPoorPopulation}
onChange={(val) => {
stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value);
}}
value={formData.totalPoorPopulation}
onChange={(e) =>
handleChange('totalPoorPopulation', e.currentTarget.value)
}
/>
<Group justify="right">

View File

@@ -1,23 +1,43 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useShallowEffect, useMediaQuery } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip as RechartsTooltip } from 'recharts';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
// ✅ BarChart Mantine
import { BarChart } from '@mantine/charts';
function JumlahPendudukMiskin() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Jumlah Penduduk Miskin'
placeholder='Cari tahun atau jumlah penduduk miskin...'
title="Jumlah Penduduk Miskin"
placeholder="Cari tahun atau jumlah penduduk miskin..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,7 +48,7 @@ function JumlahPendudukMiskin() {
}
function ListJumlahPendudukMiskin({ search }: { search: string }) {
type JPMGrafik = { id: string; year: number; totalPoorPopulation: number }
type JPMGrafik = { year: number; totalPoorPopulation: number };
const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<JPMGrafik[]>([]);
const [mounted, setMounted] = useState(false);
@@ -36,33 +56,27 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const isTablet = useMediaQuery('(max-width:1024px)');
const isMobile = useMediaQuery('(max-width:768px)');
const { data, page, loading, load, totalPages } = stateJPM.findMany;
const {
data,
page,
loading,
load,
totalPages,
} = stateJPM.findMany;
// Load data
// Load data awal
useShallowEffect(() => {
setMounted(true);
load(page, 10, search);
}, [page, search]);
// Update chart data
useEffect(() => {
if (stateJPM.findMany.data) {
setChartData(stateJPM.findMany.data.map(item => ({
id: item.id,
setChartData(
stateJPM.findMany.data.map((item) => ({
year: Number(item.year),
totalPoorPopulation: Number(item.totalPoorPopulation)
})));
totalPoorPopulation: Number(item.totalPoorPopulation),
}))
);
}
}, [stateJPM.findMany.data]);
const filteredData = data || []
const filteredData = data || [];
const handleDelete = () => {
if (selectedId) {
@@ -71,7 +85,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
setSelectedId(null);
stateJPM.findMany.load();
}
}
};
if (loading || !data) {
return (
@@ -83,15 +97,18 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
return (
<Box py={10}>
{/* Tabel */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Jumlah Penduduk Miskin</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconEdit size={18} />}
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')}
onClick={() =>
router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')
}
>
Tambah Baru
</Button>
@@ -109,22 +126,38 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? filteredData.map(item => (
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd>
<Button variant='light' color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)
}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button variant='light' color="red" disabled={stateJPM.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true) }}>
<Button
variant="light"
color="red"
disabled={stateJPM.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
)) : (
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
@@ -138,6 +171,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -153,33 +187,38 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
/>
</Center>
{/* Chart */}
{/* Bar Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Box mt="lg" style={{ width: '100%', minHeight: 350 }}>
<Title order={4} mb="sm">Grafik Jumlah Penduduk Miskin</Title>
<Title order={4} mb="sm">
Grafik Jumlah Penduduk Miskin
</Title>
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData}>
<XAxis dataKey="year" />
<YAxis />
<RechartsTooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
<BarChart
h={300}
data={chartData.map((item) => ({
name: item.year.toString(),
value: item.totalPoorPopulation,
}))}
dataKey="name"
series={[
{ name: 'value', color: colors['blue-button'] },
]}
withTooltip
valueFormatter={(v) => `${v.toLocaleString()} jiwa`}
/>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Box>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus data ini?'
text="Apakah anda yakin ingin menghapus data ini?"
/>
</Box>
);

View File

@@ -1,11 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanPendidikan() {
@@ -14,34 +14,59 @@ function EditGrafikBerdasarkanPendidikan() {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const id = params.id;
// state lokal untuk form
const [formData, setFormData] = useState({
SD: '',
SMP: '',
SMA: '',
D3: '',
S1: '',
});
useEffect(() => {
if (id) {
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data;
if (data) {
stategrafik.update.form = {
setFormData({
SD: data.SD || '',
SMP: data.SMP || '',
SMA: data.SMA || '',
D3: data.D3 || '',
S1: data.S1 || '',
};
});
}
});
}
}, [id]);
const handleChange = (field: keyof typeof formData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[field]: e.currentTarget.value,
}));
};
const handleSubmit = async () => {
stategrafik.update.id = id;
stategrafik.update.form = { ...formData }; // update global state pas submit aja
await stategrafik.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan');
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
);
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -63,36 +88,36 @@ function EditGrafikBerdasarkanPendidikan() {
label="SD"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.SD}
onChange={(val) => (stategrafik.update.form.SD = val.currentTarget.value)}
value={formData.SD}
onChange={handleChange('SD')}
/>
<TextInput
label="SMP"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.SMP}
onChange={(val) => (stategrafik.update.form.SMP = val.currentTarget.value)}
value={formData.SMP}
onChange={handleChange('SMP')}
/>
<TextInput
label="SMA"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.SMA}
onChange={(val) => (stategrafik.update.form.SMA = val.currentTarget.value)}
value={formData.SMA}
onChange={handleChange('SMA')}
/>
<TextInput
label="D3"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.D3}
onChange={(val) => (stategrafik.update.form.D3 = val.currentTarget.value)}
value={formData.D3}
onChange={handleChange('D3')}
/>
<TextInput
label="S1"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.S1}
onChange={(val) => (stategrafik.update.form.S1 = val.currentTarget.value)}
value={formData.S1}
onChange={handleChange('S1')}
/>
<Group justify="right">

View File

@@ -1,24 +1,42 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Center,
Flex,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import { DonutChart } from '@mantine/charts';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanPendidikan() {
const [search, setSearch] = useState("");
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Detail Data Pengangguran Berdasarkan Pendidikan'
placeholder='Cari data pendidikan...'
title="Detail Data Pengangguran Berdasarkan Pendidikan"
placeholder="Cari data pendidikan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -31,7 +49,6 @@ function GrafikBerdasarkanPendidikan() {
function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
@@ -45,37 +62,45 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
}
};
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => {
setMounted(true);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
if (stategrafik.findMany.data) {
const SD = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SD || 0), 0);
const SMP = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMP || 0), 0);
const SMA = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMA || 0), 0);
const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0);
const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0);
const SD = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.SD || 0),
0,
);
const SMP = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.SMP || 0),
0,
);
const SMA = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.SMA || 0),
0,
);
const D3 = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.D3 || 0),
0,
);
const S1 = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.S1 || 0),
0,
);
setDonutData([
{ name: 'SD', value: SD, color: '#4b6Ef5', key: 'SD' },
{ name: 'SMP', value: SMP, color: '#14b885', key: 'SMP' },
{ name: 'SMA', value: SMA, color: '#E6A03B', key: 'SMA' },
{ name: 'D3', value: D3, color: '#DB524D', key: 'D3' },
{ name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
{ name: 'SD', value: SD, color: '#4b6Ef5' },
{ name: 'SMP', value: SMP, color: '#14b885' },
{ name: 'SMA', value: SMA, color: '#E6A03B' },
{ name: 'D3', value: D3, color: '#DB524D' },
{ name: 'S1', value: S1, color: '#1018A8FF' },
]);
}
}, [stategrafik.findMany.data]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
@@ -87,21 +112,26 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
return (
<Box py={10}>
{/* Table Data */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Header */}
<Flex justify="space-between" align="center" mb="md">
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
<Title order={4}>List Pengangguran Berdasarkan Pendidikan</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
onClick={() =>
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create',
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Flex>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
@@ -120,7 +150,9 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<TableTr>
<TableTd colSpan={7}>
<Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text>
<Text color="dimmed">
Belum ada data grafik responden
</Text>
</Center>
</TableTd>
</TableTr>
@@ -134,7 +166,15 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<TableTd>{item.S1}</TableTd>
<TableTd>
<Tooltip label="Edit Data" withArrow>
<Button color="green" variant="light" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}>
<Button
color="green"
variant="light"
onClick={() =>
router.push(
`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`,
)
}
>
<IconEdit size={18} />
</Button>
</Tooltip>
@@ -148,7 +188,8 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}>
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
@@ -161,6 +202,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -176,51 +218,35 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
/>
</Center>
{/* Chart */}
<Box mt="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Donut Chart */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md" mt="md">
<Stack>
<Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Pendidikan</Title>
{mounted && donutData.length > 0 ? (
<Box style={{ width: '100%', minHeight: 250 }}>
<PieChart width={800} height={300} data={donutData}>
<Pie
dataKey="value"
nameKey="name"
<Title order={3} pb={10}>
Grafik Pengangguran Berdasarkan Pendidikan
</Title>
{donutData.length > 0 ? (
<DonutChart
data={donutData}
cx={400}
cy={150}
innerRadius={60}
outerRadius={115}
label
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Stack gap="xs" mt="sm">
{donutData.map((entry) => (
<Flex key={entry.key} gap="sm" align="center">
<Box w={20} h={20} bg={entry.color} />
<Text>{entry.name} : {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
withLabels
withTooltip
tooltipDataSource="segment"
size={260}
thickness={40}
/>
) : (
<Text color="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
<Text color="dimmed">
Belum ada data untuk ditampilkan dalam grafik
</Text>
)}
</Stack>
</Paper>
</Box>
{/* Modal Konfirmasi Hapus */}
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?'
text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?"
/>
</Box>
);

View File

@@ -2,39 +2,70 @@
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
import {
Box,
Button,
Paper,
Stack,
TextInput,
Title,
Group,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter();
const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const stategrafik = useProxy(
grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur
);
const id = params.id;
// ✅ state lokal, controlled
const [formData, setFormData] = useState({
usia18_25: '',
usia26_35: '',
usia36_45: '',
usia46_keatas: '',
});
// load data dari global state -> masukin ke local state
useEffect(() => {
if (id) {
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data;
if (data) {
stategrafik.update.form = {
setFormData({
usia18_25: data.usia18_25 || '',
usia26_35: data.usia26_35 || '',
usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '',
};
});
}
});
}
}, [id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// ✅ baru update global state pas submit
stategrafik.update.id = id;
stategrafik.update.form = { ...formData };
await stategrafik.update.submit();
toast.success('Data grafik berhasil diperbarui!');
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'
@@ -49,7 +80,12 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -71,40 +107,32 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
label="Usia 18 - 25"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
value={formData.usia18_25}
onChange={(e) => handleChange('usia18_25', e.currentTarget.value)}
required
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
value={formData.usia26_35}
onChange={(e) => handleChange('usia26_35', e.currentTarget.value)}
required
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
value={formData.usia36_45}
onChange={(e) => handleChange('usia36_45', e.currentTarget.value)}
required
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
value={formData.usia46_keatas}
onChange={(e) => handleChange('usia46_keatas', e.currentTarget.value)}
required
/>

View File

@@ -1,13 +1,30 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Center,
Flex,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import { DonutChart } from '@mantine/charts';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
@@ -17,8 +34,8 @@ function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
return (
<Box>
<HeaderSearch
title='Detail Data Pengangguran'
placeholder='Cari usia...'
title="Detail Data Pengangguran"
placeholder="Cari usia..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -31,7 +48,6 @@ function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
@@ -45,17 +61,10 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
}
};
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => {
setMounted(true);
load(page, 10, search)
load(page, 10, search);
}, [page, search]);
useEffect(() => {
@@ -64,16 +73,17 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const totalUsia26_35 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia26_35 || 0), 0);
const totalUsia36_45 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia36_45 || 0), 0);
const totalUsia46_keatas = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia46_keatas || 0), 0);
setDonutData([
{ name: 'usia18_25', value: totalUsia18_25, color: colors['blue-button'], key: 'usia18_25' },
{ name: 'usia26_35', value: totalUsia26_35, color: '#10A85AFF', key: 'usia26_35' },
{ name: 'usia36_45', value: totalUsia36_45, color: '#C07B13FF', key: 'usia36_45' },
{ name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' },
{ name: 'Usia 18-25', value: totalUsia18_25, color: colors['blue-button'] },
{ name: 'Usia 26-35', value: totalUsia26_35, color: '#10A85AFF' },
{ name: 'Usia 36-45', value: totalUsia36_45, color: '#C07B13FF' },
{ name: 'Usia 46+', value: totalUsia46_keatas, color: '#1094A8FF' },
]);
}
}, [stategrafik.findMany.data]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
@@ -85,24 +95,23 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
return (
<Box py={10}>
{/* Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
{/* Header */}
<Flex justify="space-between" align="center" mb="md">
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
onClick={() =>
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Flex>
{/* Table */}
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
@@ -110,26 +119,38 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
<TableTh>Usia 18-25</TableTh>
<TableTh>Usia 26-35</TableTh>
<TableTh>Usia 36-45</TableTh>
<TableTh>Usia 46 +</TableTh>
<TableTh>Usia 46+</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map(item => (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.usia18_25}</TableTd>
<TableTd>{item.usia26_35}</TableTd>
<TableTd>{item.usia36_45}</TableTd>
<TableTd>{item.usia46_keatas}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}>
<Button
color="green"
onClick={() =>
router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)
}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" disabled={stategrafik.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true); }}>
<Button
color="red"
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={20} />
</Button>
</TableTd>
@@ -147,10 +168,10 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -166,48 +187,29 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
/>
</Center>
{/* Chart */}
{/* Donut Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Usia Kerja</Title>
{mounted && donutData.length > 0 ? (
<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart width={800} height={300} data={donutData}>
<Pie dataKey="value" nameKey="name" data={donutData} cx={400} cy={150} innerRadius={60} outerRadius={115} label>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Stack mt="sm" gap="xs">
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}
</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} />
<Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}
</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} />
<Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}
</Text>
</Flex>
</Stack>
</Box>
<Title order={3} pb={10}>
Grafik Pengangguran Berdasarkan Usia Kerja
</Title>
{donutData.length > 0 ? (
<Center>
<DonutChart
data={donutData}
withLabels
withTooltip
size={200}
thickness={40}
tooltipDataSource="segment"
/>
</Center>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}

View File

@@ -1,19 +1,38 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Select, NumberInput } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Select,
NumberInput,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
// --- Helper konstanta
const MONTHS = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des',
];
function EditDetailDataPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter();
const params = useParams();
// --- state lokal form
const [formData, setFormData] = useState({
month: '',
year: new Date().getFullYear(),
@@ -23,97 +42,100 @@ function EditDetailDataPengangguran() {
percentageChange: 0,
});
// Hitung total & perubahan otomatis
const calculateTotalAndChange = async () => {
const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
// --- hitung total + persentase perubahan
const calculateTotalAndChange = useCallback(
async (data: typeof formData) => {
const total = data.educatedUnemployment + data.uneducatedUnemployment;
let percentageChange = 0;
const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const currentMonthIndex = monthOrder.indexOf(formData.month);
const currentMonthIndex = MONTHS.indexOf(data.month);
if (currentMonthIndex !== -1) {
let prevMonthIndex = currentMonthIndex - 1;
let prevYear = formData.year;
let prevYear = data.year;
if (prevMonthIndex < 0) {
prevMonthIndex = 11;
prevYear--;
}
const prevMonth = monthOrder[prevMonthIndex];
const prevData = await stateDetail.findByMonthYear.load({ month: prevMonth, year: prevYear });
const prevData = await stateDetail.findByMonthYear.load({
month: MONTHS[prevMonthIndex],
year: prevYear,
});
if (prevData && prevData.totalUnemployment > 0) {
const change = ((total - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
const change =
((total - prevData.totalUnemployment) /
prevData.totalUnemployment) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
}
return { total, percentageChange };
};
},
[stateDetail.findByMonthYear]
);
// --- update state lokal
const updateFormData = async (updates: Partial<typeof formData>) => {
const newData = { ...formData, ...updates };
const { total, percentageChange } = await calculateTotalAndChange();
const { total, percentageChange } = await calculateTotalAndChange(newData);
setFormData({ ...newData, totalUnemployment: total, percentageChange });
};
// --- load detail by ID (sekali)
useEffect(() => {
const loadDetail = async () => {
const id = params?.id as string;
if (!id) return;
try {
await stateDetail.findUnique.load(id); // ambil by ID
await stateDetail.findUnique.load(id);
const data = stateDetail.findUnique.data;
if (!data) return;
if (data) {
// Convert year from Date to number if needed
const yearValue = data.year && typeof data.year === 'object' && 'getFullYear' in data.year
const yearValue =
data.year && typeof data.year === 'object' && 'getFullYear' in data.year
? (data.year as Date).getFullYear()
: Number(data.year);
// Set the ID for update
stateDetail.update.id = id;
stateDetail.update.id = id; // simpan id untuk update
// Update Valtio state with converted year
stateDetail.update.form = {
...data,
year: yearValue,
percentageChange: data.percentageChange || 0 // Ensure it's always a number
};
// Update local formData with converted year
setFormData({
month: data.month,
year: yearValue,
totalUnemployment: data.totalUnemployment,
educatedUnemployment: data.educatedUnemployment,
uneducatedUnemployment: data.uneducatedUnemployment,
percentageChange: data.percentageChange || 0, // Ensure it's always a number
totalUnemployment: data.totalUnemployment,
percentageChange: data.percentageChange || 0,
});
}
} catch (error) {
console.error("Error loading detail:", error);
toast.error("Gagal memuat data detail");
} catch (err) {
console.error('Error loading detail:', err);
toast.error('Gagal memuat data detail');
}
};
loadDetail();
}, [params?.id]);
// --- submit form
const handleSubmit = async () => {
const { total, percentageChange } = await calculateTotalAndChange();
try {
stateDetail.update.form = { ...formData, totalUnemployment: total, percentageChange };
const { total, percentageChange } = await calculateTotalAndChange(formData);
stateDetail.update.form = {
...formData,
totalUnemployment: total,
percentageChange,
};
const success = await stateDetail.update.submit();
if (success) {
toast.success('Detail data pengangguran berhasil diperbarui!');
router.push('/admin/ekonomi/jumlah-pengangguran');
}
} catch (error) {
console.error('Error updating:', error);
} catch (err) {
console.error('Error updating:', err);
toast.error('Terjadi kesalahan saat memperbarui data');
}
};
@@ -129,36 +151,58 @@ function EditDetailDataPengangguran() {
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="lg" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Select
label="Bulan"
data={['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']}
data={MONTHS}
value={formData.month}
onChange={(val) => updateFormData({ month: val || '' })}
/>
<NumberInput
label="Tahun"
defaultValue={formData.year}
value={formData.year}
onChange={(val) => updateFormData({ year: Number(val) })}
required
/>
<TextInput
label="Pengangguran Terdidik"
type="number"
defaultValue={formData.educatedUnemployment}
onChange={(val) => updateFormData({ educatedUnemployment: Number(val.currentTarget.value) || 0 })}
value={formData.educatedUnemployment}
onChange={(e) =>
updateFormData({
educatedUnemployment: Number(e.currentTarget.value) || 0,
})
}
required
/>
<TextInput
label="Pengangguran Tidak Terdidik"
type="number"
defaultValue={formData.uneducatedUnemployment}
onChange={(val) => updateFormData({ uneducatedUnemployment: Number(val.currentTarget.value) || 0 })}
value={formData.uneducatedUnemployment}
onChange={(e) =>
updateFormData({
uneducatedUnemployment: Number(e.currentTarget.value) || 0,
})
}
required
/>
<Text fz="sm" fw={500}>Total Otomatis: {formData.totalUnemployment}</Text>
<Text fz="sm" fw={500}>Perubahan Otomatis: {formData.percentageChange !== null ? `${formData.percentageChange}%` : '-'}</Text>
<Text fz="sm" fw={500}>
Total Otomatis: {formData.totalUnemployment}
</Text>
<Text fz="sm" fw={500}>
Perubahan Otomatis:{' '}
{formData.percentageChange !== null
? `${formData.percentageChange}%`
: '-'}
</Text>
<Group justify="right">
<Button
@@ -181,4 +225,3 @@ function EditDetailDataPengangguran() {
}
export default EditDetailDataPengangguran;

View File

@@ -26,15 +26,17 @@ function EditLowonganKerja() {
const params = useParams();
const [formData, setFormData] = useState({
posisi: lowonganKerjaState.update.form.posisi,
namaPerusahaan: lowonganKerjaState.update.form.namaPerusahaan,
lokasi: lowonganKerjaState.update.form.lokasi,
tipePekerjaan: lowonganKerjaState.update.form.tipePekerjaan,
gaji: lowonganKerjaState.update.form.gaji,
deskripsi: lowonganKerjaState.update.form.deskripsi,
kualifikasi: lowonganKerjaState.update.form.kualifikasi,
posisi: '',
namaPerusahaan: '',
lokasi: '',
tipePekerjaan: '',
gaji: '',
deskripsi: '',
kualifikasi: '',
notelp: '',
});
// load data sekali aja ketika mount / id berubah
useEffect(() => {
const loadLowongan = async () => {
const id = params?.id as string;
@@ -51,6 +53,7 @@ function EditLowonganKerja() {
gaji: data.gaji || '',
deskripsi: data.deskripsi || '',
kualifikasi: data.kualifikasi || '',
notelp: data.notelp || '',
});
}
} catch (error) {
@@ -62,14 +65,17 @@ function EditLowonganKerja() {
loadLowongan();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
lowonganState.update.id = params?.id as string;
lowonganState.update.form = {
...lowonganState.update.form,
...formData,
};
lowonganState.update.form = { ...formData };
await lowonganState.update.update();
toast.success("Lowongan kerja berhasil diperbarui!");
@@ -107,40 +113,48 @@ function EditLowonganKerja() {
<TextInput
label="Posisi"
placeholder="Masukkan posisi"
defaultValue={formData.posisi}
onChange={(e) => setFormData({ ...formData, posisi: e.target.value })}
value={formData.posisi}
onChange={(e) => handleChange("posisi", e.target.value)}
required
/>
<TextInput
label="Nama Perusahaan"
placeholder="Masukkan nama perusahaan"
defaultValue={formData.namaPerusahaan}
onChange={(e) => setFormData({ ...formData, namaPerusahaan: e.target.value })}
value={formData.namaPerusahaan}
onChange={(e) => handleChange("namaPerusahaan", e.target.value)}
required
/>
<TextInput
label="Lokasi"
placeholder="Masukkan lokasi"
defaultValue={formData.lokasi}
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
value={formData.lokasi}
onChange={(e) => handleChange("lokasi", e.target.value)}
required
/>
<TextInput
label="Nomor Yang Dapat Dihubungi"
placeholder="Masukkan nomor yang dapat dihubungi"
value={formData.notelp}
onChange={(e) => handleChange("notelp", e.target.value)}
required
/>
<TextInput
label="Tipe Pekerjaan"
placeholder="Masukkan tipe pekerjaan"
defaultValue={formData.tipePekerjaan}
onChange={(e) => setFormData({ ...formData, tipePekerjaan: e.target.value })}
value={formData.tipePekerjaan}
onChange={(e) => handleChange("tipePekerjaan", e.target.value)}
required
/>
<TextInput
label="Gaji (per bulan)"
placeholder="Masukkan gaji"
defaultValue={formData.gaji}
onChange={(e) => setFormData({ ...formData, gaji: e.target.value })}
value={formData.gaji}
onChange={(e) => handleChange("gaji", e.target.value)}
required
/>
@@ -150,7 +164,7 @@ function EditLowonganKerja() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={(val) => handleChange("deskripsi", val)}
/>
</Box>
@@ -160,7 +174,7 @@ function EditLowonganKerja() {
</Text>
<EditEditor
value={formData.kualifikasi}
onChange={(val) => setFormData({ ...formData, kualifikasi: val })}
onChange={(val) => handleChange("kualifikasi", val)}
/>
</Box>

View File

@@ -82,6 +82,11 @@ function DetailLowonganKerjaLokal() {
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Nomor Yang Dapat Dihubungi</Text>
<Text fz="md" c="dimmed">{data.notelp || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tipe Pekerjaan</Text>
<Text fz="md" c="dimmed">{data.tipePekerjaan || '-'}</Text>

View File

@@ -30,6 +30,7 @@ function CreateLowonganKerja() {
gaji: '',
deskripsi: '',
kualifikasi: '',
notelp: '',
};
};
@@ -86,6 +87,15 @@ function CreateLowonganKerja() {
placeholder="Masukkan nama perusahaan"
required
/>
<TextInput
defaultValue={lowonganState.create.form.notelp}
onChange={(val) =>
(lowonganState.create.form.notelp = val.target.value)
}
label="Nomor Yang Dapat Dihubungi"
placeholder="Masukkan nomor yang dapat dihubungi"
required
/>
<TextInput
defaultValue={lowonganState.create.form.lokasi}
onChange={(val) =>

View File

@@ -25,9 +25,8 @@ function EditKategoriProduk() {
const id = params?.id as string;
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [formData, setFormData] = useState({
nama: '',
});
const [formData, setFormData] = useState({ nama: '' });
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadKategoriProduk = async () => {
@@ -37,21 +36,30 @@ function EditKategoriProduk() {
const data = await statePasar.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
// simpan id ke state global hanya untuk referensi
statePasar.edit.id = id;
setFormData({
nama: data.nama || '',
});
// simpan data ke state lokal
setFormData({ nama: data.nama || '' });
}
} catch (error) {
console.error('Error loading kategori produk:', error);
toast.error('Gagal memuat data kategori produk');
} finally {
setLoading(false);
}
};
loadKategoriProduk();
}, [id]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
@@ -59,10 +67,8 @@ function EditKategoriProduk() {
return;
}
statePasar.edit.form = {
nama: formData.nama.trim(),
};
// update global state hanya saat submit
statePasar.edit.form = { nama: formData.nama.trim() };
if (!statePasar.edit.id) {
statePasar.edit.id = id; // fallback
}
@@ -79,12 +85,21 @@ function EditKategoriProduk() {
}
};
if (loading) {
return <Text>Loading...</Text>;
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -104,10 +119,11 @@ function EditKategoriProduk() {
>
<Stack gap="md">
<TextInput
name="nama"
label={<Text fw="bold" fz="sm">Nama Kategori Produk</Text>}
placeholder="Masukkan nama kategori produk"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={handleChange}
required
/>

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
@@ -24,6 +24,16 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
type FormData = {
nama: string;
harga: number;
alamatUsaha: string;
imageId: string;
rating: number;
kategoriId: string[];
kontak: string;
};
function EditPasarDesa() {
const pasarState = useProxy(pasarDesaState);
const router = useRouter();
@@ -31,17 +41,20 @@ function EditPasarDesa() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: pasarState.pasarDesa.edit.form.nama || '',
harga: pasarState.pasarDesa.edit.form.harga || 0,
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || '',
imageId: pasarState.pasarDesa.edit.form.imageId || '',
rating: pasarState.pasarDesa.edit.form.rating || 0,
kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
const [formData, setFormData] = useState<FormData>({
nama: '',
harga: 0,
alamatUsaha: '',
imageId: '',
rating: 0,
kategoriId: [],
kontak: '',
});
// load data awal
useEffect(() => {
pasarState.kategoriProduk.findMany.load();
pasarState.kategoriProduk.findManyAll.load();
const loadPasarDesa = async () => {
const id = params?.id as string;
if (!id) return;
@@ -56,6 +69,7 @@ function EditPasarDesa() {
imageId: data.imageId || '',
rating: data.rating || 0,
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
kontak: data.kontak || '',
});
if (data.image?.link) setPreviewImage(data.image.link);
}
@@ -70,19 +84,27 @@ function EditPasarDesa() {
loadPasarDesa();
}, [params?.id]);
const handleChange = (key: keyof FormData, value: any) => {
setFormData((prev) => ({ ...prev, [key]: value }));
};
const handleSubmit = async () => {
try {
pasarState.pasarDesa.edit.form = { ...pasarState.pasarDesa.edit.form, ...formData };
// upload image kalau ada file baru
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
pasarState.pasarDesa.edit.form.imageId = uploaded.id;
imageId = uploaded.id;
}
// update global state hanya saat submit
pasarState.pasarDesa.edit.form = {
...formData,
imageId,
};
await pasarState.pasarDesa.edit.update();
toast.success('Pasar desa berhasil diperbarui!');
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
@@ -114,6 +136,7 @@ function EditPasarDesa() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Dropzone upload */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Produk
@@ -170,11 +193,12 @@ function EditPasarDesa() {
)}
</Box>
{/* Controlled Inputs */}
<TextInput
label="Nama Produk"
placeholder="Masukkan nama produk"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
@@ -182,8 +206,8 @@ function EditPasarDesa() {
type="number"
label="Harga Produk"
placeholder="Masukkan harga produk"
defaultValue={formData.harga}
onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
value={formData.harga}
onChange={(e) => handleChange('harga', Number(e.target.value))}
required
/>
@@ -194,16 +218,24 @@ function EditPasarDesa() {
step={0.1}
label="Rating Produk"
placeholder="Masukkan rating produk (0-5)"
defaultValue={formData.rating}
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
value={formData.rating}
onChange={(e) => handleChange('rating', Number(e.target.value))}
required
/>
<TextInput
label="Alamat Usaha"
placeholder="Masukkan alamat usaha"
defaultValue={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
value={formData.alamatUsaha}
onChange={(e) => handleChange('alamatUsaha', e.target.value)}
required
/>
<TextInput
label="Kontak"
placeholder="Masukkan kontak"
value={formData.kontak}
onChange={(e) => handleChange('kontak', e.target.value)}
required
/>
@@ -211,9 +243,9 @@ function EditPasarDesa() {
label="Kategori Produk"
placeholder="Pilih kategori produk"
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
onChange={(val) => handleChange('kategoriId', val)}
data={
pasarState.kategoriProduk.findMany.data?.map((v) => ({
pasarState.kategoriProduk.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []

View File

@@ -85,6 +85,11 @@ function DetailPasarDesa() {
<Text fz="md" c="dimmed">{data.alamatUsaha || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Kontak</Text>
<Text fz="md" c="dimmed">{data.kontak || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (

View File

@@ -30,7 +30,7 @@ export default function CreatePasarDesa() {
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
statePasar.kategoriProduk.findMany.load();
statePasar.kategoriProduk.findManyAll.load();
}, []);
const resetForm = () => {
@@ -41,6 +41,7 @@ export default function CreatePasarDesa() {
imageId: '',
rating: 0,
kategoriId: [],
kontak: '',
};
setPreviewImage(null);
setFile(null);
@@ -184,6 +185,15 @@ export default function CreatePasarDesa() {
onChange={(e) => (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)}
/>
{/* Kontak */}
<TextInput
label="Kontak"
type="number"
placeholder="Masukkan kontak"
defaultValue={statePasar.pasarDesa.create.form.kontak}
onChange={(e) => (statePasar.pasarDesa.create.form.kontak = e.target.value)}
/>
{/* Kategori Produk */}
<MultiSelect
label="Kategori Produk"
@@ -191,7 +201,7 @@ export default function CreatePasarDesa() {
value={statePasar.pasarDesa.create.form.kategoriId}
onChange={(val) => (statePasar.pasarDesa.create.form.kategoriId = val)}
data={
statePasar.kategoriProduk.findMany.data?.map((v) => ({
statePasar.kategoriProduk.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
@@ -19,53 +18,105 @@ import {
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
type Statistik = {
tahun: string;
jumlah: string;
};
type FormData = {
nama: string;
deskripsi: string;
icon: string;
statistik: Statistik;
};
const initialForm: FormData = {
nama: '',
deskripsi: '',
icon: '',
statistik: {
tahun: '',
jumlah: '',
},
};
function EditProgramKemiskinan() {
const router = useRouter();
const params = useParams() as { id: string };
const { id } = useParams() as { id: string };
const stateProgram = useProxy(programKemiskinanState);
const id = params.id;
const [formData, setFormData] = useState<FormData>(initialForm);
// Load data 1x dari global state → isi local state
useEffect(() => {
if (id) {
stateProgram.findUnique.load(id).then(() => {
if (!id) return;
const loadData = async () => {
try {
await stateProgram.findUnique.load(id);
const data = stateProgram.findUnique.data;
if (data) {
stateProgram.update.form = {
nama: data.nama || '',
deskripsi: data.deskripsi || '',
icon: data.icon || '',
setFormData({
nama: data.nama ?? '',
deskripsi: data.deskripsi ?? '',
icon: data.icon ?? '',
statistik: {
tahun: data.statistik?.tahun?.toString() || '',
jumlah: data.statistik?.jumlah?.toString() || '',
tahun: data.statistik?.tahun?.toString() ?? '',
jumlah: data.statistik?.jumlah?.toString() ?? '',
},
};
}
}).catch((err) => {
console.error("Error load data:", err);
toast.error("Gagal mengambil data program");
});
}
}, [id]);
} catch (err) {
console.error('Error load data:', err);
toast.error('Gagal mengambil data program');
}
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); // ✅ hanya trigger saat id berubah
// generic handler untuk field top-level
const handleChange = useCallback(
(field: keyof FormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
// khusus nested statistik
const handleStatistikChange = useCallback(
(field: keyof Statistik, value: string) => {
setFormData((prev) => ({
...prev,
statistik: { ...prev.statistik, [field]: value },
}));
},
[]
);
const handleSubmit = async () => {
try {
stateProgram.update.id = id;
stateProgram.update.form = formData;
await stateProgram.update.update();
toast.success("Program berhasil diperbarui!");
toast.success('Program berhasil diperbarui!');
router.push('/admin/ekonomi/program-kemiskinan');
} catch (error) {
console.error("Error update program:", error);
toast.error("Terjadi kesalahan saat memperbarui program");
console.error('Error update program:', error);
toast.error('Terjadi kesalahan saat memperbarui program');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol kembali */}
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
@@ -92,10 +143,8 @@ function EditProgramKemiskinan() {
>
<Stack gap="md">
<TextInput
defaultValue={stateProgram.update.form.nama}
onChange={(e) => {
stateProgram.update.form.nama = e.target.value;
}}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
label={<Text fw="bold" fz="sm">Judul Program</Text>}
placeholder="Masukkan judul program"
required
@@ -106,10 +155,8 @@ function EditProgramKemiskinan() {
Deskripsi
</Text>
<EditEditor
value={stateProgram.update.form.deskripsi}
onChange={(val) => {
stateProgram.update.form.deskripsi = val;
}}
value={formData.deskripsi}
onChange={(val) => handleChange('deskripsi', val)}
/>
</Box>
@@ -118,10 +165,8 @@ function EditProgramKemiskinan() {
Ikon Program Kreatif Desa
</Text>
<SelectIconProgramEdit
value={stateProgram.update.form.icon as IconKey}
onChange={(value) => {
stateProgram.update.form.icon = value;
}}
value={formData.icon as IconKey}
onChange={(val) => handleChange('icon', val)}
/>
</Box>
@@ -131,10 +176,8 @@ function EditProgramKemiskinan() {
</Text>
<TextInput
type="number"
defaultValue={stateProgram.update.form.statistik.jumlah}
onChange={(e) => {
stateProgram.update.form.statistik.jumlah = e.target.value;
}}
value={formData.statistik.jumlah}
onChange={(e) => handleStatistikChange('jumlah', e.target.value)}
label="Jumlah Masyarakat Miskin"
placeholder="Masukkan jumlah masyarakat miskin"
required
@@ -142,10 +185,8 @@ function EditProgramKemiskinan() {
<TextInput
type="number"
defaultValue={stateProgram.update.form.statistik.tahun}
onChange={(e) => {
stateProgram.update.form.statistik.tahun = e.target.value;
}}
value={formData.statistik.tahun}
onChange={(e) => handleStatistikChange('tahun', e.target.value)}
label="Tahun"
placeholder="Masukkan tahun"
required

View File

@@ -16,7 +16,7 @@ import {
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
@@ -28,28 +28,46 @@ function EditSektorUnggulanDesa() {
const id = params.id;
// state lokal buat form
const [formData, setFormData] = useState({
name: '',
description: '',
value: 0,
});
// Load data saat komponen mount
useEffect(() => {
if (id) {
stateGrafik.findUnique.load(id).then(() => {
stateGrafik.findUnique
.load(id)
.then(() => {
const data = stateGrafik.findUnique.data;
if (data) {
stateGrafik.update.form = {
setFormData({
name: data.name || '',
description: data.description || '',
value: data.value || 0,
};
});
}
}).catch((err) => {
})
.catch((err) => {
console.error('Error load sektor unggulan:', err);
toast.error('Gagal mengambil data sektor unggulan');
});
}
}, [id]);
const handleChange =
(field: keyof typeof formData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = field === 'value' ? Number(e.currentTarget.value) : e.currentTarget.value;
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
stateGrafik.update.id = id;
stateGrafik.update.form = { ...formData }; // update global pas submit
await stateGrafik.update.submit();
toast.success('Sektor unggulan berhasil diperbarui!');
router.push('/admin/ekonomi/sektor-unggulan-desa');
@@ -89,10 +107,8 @@ function EditSektorUnggulanDesa() {
<TextInput
label="Nama Sektor Unggulan"
placeholder="Masukkan nama sektor unggulan"
defaultValue={stateGrafik.update.form.name}
onChange={(val) => {
stateGrafik.update.form.name = val.currentTarget.value;
}}
value={formData.name}
onChange={handleChange('name')}
required
/>
<Box>
@@ -100,20 +116,18 @@ function EditSektorUnggulanDesa() {
Konten
</Text>
<EditEditor
value={stateGrafik.update.form.description}
onChange={(htmlContent) => {
stateGrafik.update.form.description = htmlContent;
}}
value={formData.description}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, description: htmlContent }))
}
/>
</Box>
<TextInput
label="Jumlah"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stateGrafik.update.form.value}
onChange={(val) => {
stateGrafik.update.form.value = Number(val.currentTarget.value);
}}
value={formData.value}
onChange={handleChange('value')}
required
/>

View File

@@ -2,6 +2,7 @@
'use client'
import colors from '@/con/colors';
import {
ScrollArea,
Stack,
Tabs,
TabsList,
@@ -9,28 +10,20 @@ import {
TabsTab,
Title,
Tooltip,
ScrollArea,
} from '@mantine/core';
import {
IconBuildingCommunity,
IconHierarchy,
IconUsers
} from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import {
IconHierarchy,
IconUsers,
IconGitBranch,
} from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
icon: <IconHierarchy size={18} stroke={1.8} />,
tooltip: "Kelola daftar posisi organisasi",
},
{
label: "Pegawai",
value: "pegawai",
@@ -39,12 +32,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
tooltip: "Kelola data pegawai BUMDesa",
},
{
label: "Hubungan Organisasi",
value: "hubunganorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi",
icon: <IconGitBranch size={18} stroke={1.8} />,
tooltip: "Atur hubungan antar posisi organisasi",
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
icon: <IconHierarchy size={18} stroke={1.8} />,
tooltip: "Kelola daftar posisi organisasi",
},
{
label: "Struktur Organisasi",
value: "strukturorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi",
icon: <IconBuildingCommunity size={18} stroke={1.8} />,
tooltip: "Kelola struktur organisasi BUMDesa"
}
];
const currentTab = tabs.find((tab) => tab.href === pathname);

View File

@@ -1,94 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Box, Button, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
export default function EditHubunganOrganisasi() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const state = useProxy(strukturorganisasiState.hubunganOrganisasi);
const pegawaiList = strukturorganisasiState.pegawai.findMany.data;
const [form, setForm] = useState({
atasanId: '',
bawahanId: '',
tipe: '',
});
useEffect(() => {
strukturorganisasiState.pegawai.findMany.load();
if (id) {
state.edit.load(id).then(data => {
if (data) {
setForm({
atasanId: data.atasanId,
bawahanId: data.bawahanId,
tipe: data.tipe || '',
});
}
});
}
}, [id]);
const handleSubmit = async () => {
if (!form.atasanId || !form.bawahanId) {
toast.warn("Atasan dan bawahan harus diisi");
return;
}
state.edit.id = id;
state.edit.form = form;
const result = await state.edit.update();
if (result) {
toast.success("Data berhasil diperbarui");
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi');
}
};
return (
<Box>
<Paper p="md" w={{ base: '100%', md: '50%' }}>
<Stack>
<Title order={3}>Edit Hubungan Organisasi</Title>
<Select
label="Atasan"
placeholder="Pilih atasan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.atasanId}
onChange={(val) => setForm({ ...form, atasanId: val || '' })}
/>
<Select
label="Bawahan"
placeholder="Pilih bawahan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.bawahanId}
onChange={(val) => setForm({ ...form, bawahanId: val || '' })}
/>
<TextInput
label="Tipe"
placeholder="Contoh: langsung_melapor"
defaultValue={form.tipe}
onChange={(e) => setForm({ ...form, tipe: e.currentTarget.value })}
/>
<Button onClick={handleSubmit} color="blue">Simpan</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,78 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Box, Button, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
export default function CreateHubunganOrganisasi() {
const router = useRouter();
const state = useProxy(strukturorganisasiState.hubunganOrganisasi);
const pegawaiList = strukturorganisasiState.pegawai.findMany.data;
const [form, setForm] = useState({
atasanId: '',
bawahanId: '',
tipe: '',
});
useEffect(() => {
strukturorganisasiState.pegawai.findMany.load();
}, []);
const handleSubmit = async () => {
if (!form.atasanId || !form.bawahanId) {
toast.warn("Atasan dan bawahan harus diisi");
return;
}
state.create.form = form;
const result = await state.create.create();
if (result) {
toast.success("Hubungan Organisasi berhasil ditambahkan");
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi');
}
};
return (
<Box>
<Paper p="md" w={{ base: '100%', md: '50%' }}>
<Stack>
<Title order={3}>Create Hubungan Organisasi</Title>
<Select
label="Atasan"
placeholder="Pilih atasan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.atasanId}
onChange={(val) => setForm({ ...form, atasanId: val || '' })}
/>
<Select
label="Bawahan"
placeholder="Pilih bawahan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
value={form.bawahanId}
onChange={(val) => setForm({ ...form, bawahanId: val || '' })}
/>
<TextInput
label="Tipe"
placeholder="Contoh: langsung_melapor"
defaultValue={form.tipe}
onChange={(e) => setForm({ ...form, tipe: e.currentTarget.value })}
/>
<Button onClick={handleSubmit} color="blue">Simpan</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,130 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function HubunganOrganisasi() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Hubungan Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListHubunganOrganisasi search={search} />
</Box>
);
}
function ListHubunganOrganisasi({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.hubunganOrganisasi);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
useShallowEffect(() => {
stateOrganisasi.findMany.load();
}, []);
const handleHapus = () => {
if (selectedId) {
stateOrganisasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.atasan?.namaLengkap?.toLowerCase().includes(keyword) ||
item.bawahan?.namaLengkap?.toLowerCase().includes(keyword) ||
item.tipe?.toLowerCase().includes(keyword)
);
});
if (!stateOrganisasi.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Hubungan Organisasi'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Atasan</TableTh>
<TableTh>Bawahan</TableTh>
<TableTh>Tipe</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData
.sort((a, b) =>
a.atasan?.namaLengkap.localeCompare(b.atasan?.namaLengkap)
)
.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.atasan?.namaLengkap}</TableTd>
<TableTd>{item.bawahan?.namaLengkap}</TableTd>
<TableTd>{item.tipe}</TableTd>
<TableTd>
<Button
bg="green"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/${item.id}`)}
>
<IconEdit size={25} />
</Button>
</TableTd>
<TableTd>
<Button
color="red"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus hubungan organisasi ini?'
/>
</Box>
);
}
export default HubunganOrganisasi;

View File

@@ -1,7 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
@@ -12,7 +14,8 @@ import {
Stack,
Text,
TextInput,
Title
Title,
Tooltip
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -21,42 +24,26 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface PreviewImage {
file?: File;
preview: string;
}
interface PegawaiFormData {
namaLengkap: string;
gelarAkademik: string;
imageId: string | null;
tanggalMasuk: string;
email: string;
telepon: string;
alamat: string;
posisiId: string;
isActive: boolean;
}
export default function EditPegawai() {
export default function EditPegawaiBumDes() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const [previewImage, setPreviewImage] = useState<PreviewImage | string | null>(null);
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
const [formData, setFormData] = useState<PegawaiFormData>({
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const [formData, setFormData] = useState({
namaLengkap: '',
gelarAkademik: '',
imageId: '',
tanggalMasuk: '',
email: '',
telepon: '',
alamat: '',
posisiId: '',
isActive: true,
});
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// Format date to YYYY-MM-DD for date input
// Format date for <input type="date">
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
@@ -64,34 +51,29 @@ export default function EditPegawai() {
};
useEffect(() => {
strukturorganisasiState.posisiOrganisasi.findMany.load();
const loadPegawai = async () => {
try {
await stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
const data = await stateOrganisasi.edit.load(id);
if (data) {
setFormData({
namaLengkap: data.namaLengkap || "",
gelarAkademik: data.gelarAkademik || "",
imageId: data.imageId || "",
tanggalMasuk: data.tanggalMasuk || "",
email: data.email || "",
telepon: data.telepon || "",
alamat: data.alamat || "",
posisiId: data.posisiId || "",
isActive: data.isActive ?? true, // pakai nullish coalescing
namaLengkap: data.namaLengkap || '',
gelarAkademik: data.gelarAkademik || '',
imageId: data.imageId || '',
tanggalMasuk: data.tanggalMasuk || '',
email: data.email || '',
telepon: data.telepon || '',
alamat: data.alamat || '',
posisiId: data.posisiId || '',
isActive: data.isActive ?? true,
});
if (data.image?.link) {
setPreviewImage(data.image.link);
} else {
setPreviewImage(null);
}
setPreviewImage(data.image?.link || null);
}
} catch (error) {
console.error("Error loading pegawai:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data pegawai"
);
console.error('Error loading pegawai:', error);
toast.error(error instanceof Error ? error.message : 'Gagal mengambil data pegawai');
}
};
@@ -101,182 +83,187 @@ export default function EditPegawai() {
const handleSubmit = async () => {
try {
if (!formData.namaLengkap.trim()) {
toast.error('Nama lengkap tidak boleh kosong');
return;
return toast.error('Nama lengkap tidak boleh kosong');
}
stateOrganisasi.edit.form = {
namaLengkap: formData.namaLengkap.trim(),
gelarAkademik: formData.gelarAkademik.trim(),
imageId: formData.imageId ? formData.imageId.trim() : "",
tanggalMasuk: formData.tanggalMasuk.trim(),
email: formData.email.trim(),
telepon: formData.telepon.trim(),
alamat: formData.alamat.trim(),
posisiId: formData.posisiId.trim(),
isActive: formData.isActive,
};
// Update global state only on submit
const updatedForm = { ...formData };
if (id && !stateOrganisasi.edit.id) {
stateOrganisasi.edit.id = id;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
updatedForm.imageId = uploaded.id;
}
stateOrganisasi.edit.form = updatedForm;
if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id;
const success = await stateOrganisasi.edit.submit();
if (success) {
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
}
if (success) router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai');
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pegawai");
console.error('Error updating pegawai:', error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
</Tooltip>
<Title order={4} ml="sm" c="dark">Edit Data Pegawai PPID</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Data Pegawai</Title>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama Lengkap */}
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
defaultValue={formData.namaLengkap}
value={formData.namaLengkap}
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
required
/>
{/* Gelar Akademik */}
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
defaultValue={formData.gelarAkademik}
value={formData.gelarAkademik}
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
/>
{/* Foto Profil */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Text fw="bold" fz="sm" mb={6}>Foto Profil</Text>
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
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={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
<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={typeof previewImage === 'string' ? previewImage : previewImage?.preview}
alt="Preview"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
loading="lazy"
/>
</Box>
)}
</Box>
</Box>
{/* Tanggal Masuk */}
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
defaultValue={formatDateForInput(formData.tanggalMasuk)}
value={formatDateForInput(formData.tanggalMasuk)}
onChange={(e) => setFormData({ ...formData, tanggalMasuk: e.target.value })}
/>
{/* Email */}
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
defaultValue={formData.email}
onChange={(e) => (formData.email = e.currentTarget.value)}
type="email"
placeholder="contoh@email.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
{/* Telepon */}
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
defaultValue={formData.telepon}
onChange={(e) => (formData.telepon = e.currentTarget.value)}
placeholder="08123456789"
value={formData.telepon}
onChange={(e) => setFormData({ ...formData, telepon: e.target.value })}
/>
{/* Alamat */}
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
defaultValue={formData.alamat}
onChange={(e) => (formData.alamat = e.currentTarget.value)}
placeholder="Jl. Contoh No. 123"
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
/>
{/* Posisi */}
<Box>
<Text fw="bold" fz="sm" mb={6}>Posisi</Text>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={
strukturorganisasiState.posisiOrganisasi.findMany.data?.map((p) => ({
value: p.id, // harus string
label: p.nama,
})) || []
}
data={stateStrukturBumDes.posisiOrganisasi.findManyAll.data?.map(p => ({ value: p.id, label: p.nama })) || []}
value={formData.posisiId}
onChange={(value) => {
if (value !== null) {
setFormData({ ...formData, posisiId: value }); // value harus string
}
}}
onChange={(value) => value && setFormData({ ...formData, posisiId: value })}
searchable
clearable
/>
</Box>
{/* Status Pegawai */}
<Box>
<Text fw="bold" fz="sm" mb={6}>Status Pegawai</Text>
<Select
label="Status Pegawai"
data={[
{ value: 'true', label: 'Aktif' },
{ value: 'false', label: 'Tidak Aktif' },
]}
value={String(formData.isActive)} // 'true' atau 'false'
onChange={(val) => {
setFormData({ ...formData, isActive: val === 'true' });
}}
value={String(formData.isActive)}
onChange={(val) => setFormData({ ...formData, isActive: val === 'true' })}
clearable
/>
</Box>
<Group>
{/* Submit Button */}
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
color="blue"
loading={stateOrganisasi.edit.loading}
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)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box >
</Box>
);
}

View File

@@ -1,42 +1,56 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailPegawai() {
const statePegawai = useProxy(strukturorganisasiState.pegawai)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const statePegawai = useProxy(stateStrukturBumDes.pegawai);
const [modalHapus, setModalHapus] = useState(false);
const [modalNonActive, setModalNonActive] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
statePegawai.findUnique.load(params?.id as string)
}, [])
stateStrukturBumDes.posisiOrganisasi.findMany.load();
statePegawai.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
statePegawai.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai")
statePegawai.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
}
};
const handleNonActive = () => {
if (selectedId) {
statePegawai.nonActive.byId(selectedId);
setModalNonActive(false);
setSelectedId(null);
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
}
};
if (!statePegawai.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = statePegawai.findUnique.data;
return (
<Box>
<Box mb={10}>
@@ -44,91 +58,125 @@ function DetailPegawai() {
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pegawai</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Lengkap</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.namaLengkap}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gelar Akademik</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.gelarAkademik}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Image</Text>
{statePegawai.findUnique.data?.image?.link ? (
<Image src={statePegawai.findUnique.data?.image?.link} alt='' loading="lazy" />
) : (
<Text fz={"md"} c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal Masuk</Text>
<Text fz={"lg"}>
{statePegawai.findUnique.data?.tanggalMasuk
? new Date(statePegawai.findUnique.data.tanggalMasuk).toLocaleDateString()
: "-"}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pegawai PPID
</Text>
</Box>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="md">
<Box>
<Text fz={"lg"} fw={"bold"}>Email</Text>
<Text fz={"lg"} style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{statePegawai.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Telepon</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.telepon}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"} style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{statePegawai.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Posisi</Text>
<Stack gap={4}>
{statePegawai.findUnique.data?.posisi ? (
<Text fz={"lg"}>
{statePegawai.findUnique.data.posisi.nama}
<Text fz="lg" fw="bold">Nama Lengkap</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Text>
) : (
<Text fz={"lg"} c="dimmed">
Tidak ada posisi
</Text>
)}
</Stack>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Aktif</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.isActive ? "Ya" : "Tidak"}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
setSelectedId(statePegawai.findUnique.data.id);
setModalHapus(true);
}
<Text fz="lg" fw="bold">Posisi</Text>
<Text fz="md" c="dimmed">{data.posisi?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Email</Text>
<Text fz="md" c="dimmed">{data.email || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Telepon</Text>
<Text fz="md" c="dimmed">{data.telepon || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.alamat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tanggal Masuk</Text>
<Text fz="md" c="dimmed">
{data.tanggalMasuk ? new Date(data.tanggalMasuk).toLocaleDateString() : '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Status</Text>
<Text fz="md" c={data.isActive ? 'green' : 'red'}>
{data.isActive ? 'Aktif' : 'Tidak Aktif'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Foto Profil</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.namaLengkap || 'Foto Profil'}
w={200}
h={200}
radius="md"
fit="cover"
mt="sm"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
}}
disabled={!statePegawai.findUnique.data}
color="red">
<IconX size={20} />
loading='lazy'
/>
) : (
<Text fz="sm" c="dimmed" mt="sm">Tidak ada foto profil</Text>
)}
</Box>
<Button
color={data.isActive ? "green" : "gray"}
onClick={() => {
setSelectedId(data.id || null);
setModalNonActive(true);
}}
variant="light"
radius="md"
size="md"
>
{data.isActive ? "Aktif" : "Nonaktif"}
</Button>
<Group gap="sm" mt="md">
<Tooltip label="Hapus Pegawai" withArrow position="top">
<Button
color="red"
onClick={() => {
if (statePegawai.findUnique.data) {
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
}
setSelectedId(data.id || null);
setModalHapus(true);
}}
disabled={!statePegawai.findUnique.data}
color="green">
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Pegawai" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
@@ -139,7 +187,15 @@ function DetailPegawai() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus produk ini?"
text="Apakah Anda yakin ingin menghapus data pegawai ini?"
/>
{/* Modal NonActive */}
<ModalKonfirmasiHapus
opened={modalNonActive}
onClose={() => setModalNonActive(false)}
onConfirm={handleNonActive}
text="Apakah Anda yakin ingin menonaktifkan pegawai ini?"
/>
</Box>
);

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -11,17 +12,17 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePegawai() {
function CreatePegawaiBumDes() {
const router = useRouter();
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const stateOrganisasi = useProxy(strukturorganisasiState)
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai)
useEffect(() => {
stateOrganisasi.posisiOrganisasi.findMany.load();
stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
resetForm();
}, []);
const resetForm = () => {
stateOrganisasi.pegawai.create.form = {
stateOrganisasi.create.form = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
@@ -52,13 +53,13 @@ function CreatePegawai() {
}
// Set status aktif secara otomatis
stateOrganisasi.pegawai.create.form.isActive = true;
stateOrganisasi.create.form.isActive = true;
// Simpan ID gambar ke form
stateOrganisasi.pegawai.create.form.imageId = uploaded.id;
stateOrganisasi.create.form.imageId = uploaded.id;
// Submit form
await stateOrganisasi.pegawai.create.submit();
await stateOrganisasi.create.submit();
// Reset form dan redirect
@@ -72,33 +73,51 @@ function CreatePegawai() {
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Pegawai</Title>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Pegawai BUMDesa
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
defaultValue={stateOrganisasi.pegawai.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.pegawai.create.form.namaLengkap = e.currentTarget.value)}
defaultValue={stateOrganisasi.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
required
/>
</Box>
<Box>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
defaultValue={stateOrganisasi.pegawai.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.pegawai.create.form.gelarAkademik = e.currentTarget.value)}
defaultValue={stateOrganisasi.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Text fw="bold" fz="sm" mb={6}>
Foto Profil
</Text>
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
const file = files[0];
if (file) {
setPreviewImage({
file,
@@ -110,8 +129,20 @@ function CreatePegawai() {
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
styles={{
root: {
border: '2px dashed #ced4da',
borderRadius: '8px',
padding: '20px',
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: '#228be6',
},
},
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={160} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
@@ -123,79 +154,113 @@ function CreatePegawai() {
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
<Text size="md" inline>
Seret gambar ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
Format yang didukung: JPG, PNG, WebP. Maksimal 5MB
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="md">
<Text fw="bold" fz="sm" mb={6}>
Preview Gambar
</Text>
<Image
src={previewImage.preview}
alt="Preview"
width={280}
height={180}
width={200}
height={200}
fit="cover"
radius="sm"
mt="md"
loading="lazy"
radius="md"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
}}
loading='lazy'
/>
</Box>
)}
</Box>
</Box>
<Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
defaultValue={stateOrganisasi.pegawai.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.pegawai.create.form.tanggalMasuk = e.currentTarget.value)}
defaultValue={stateOrganisasi.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
/>
</Box>
<Box>
<TextInput
label="Email"
type="email"
placeholder="Contoh: email@example.com"
defaultValue={stateOrganisasi.pegawai.create.form.email}
onChange={(e) => (stateOrganisasi.pegawai.create.form.email = e.currentTarget.value)}
defaultValue={stateOrganisasi.create.form.email}
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
/>
</Box>
<Box>
<TextInput
label="Telepon"
label="Nomor Telepon"
placeholder="Contoh: 08123456789"
defaultValue={stateOrganisasi.pegawai.create.form.telepon}
onChange={(e) => (stateOrganisasi.pegawai.create.form.telepon = e.currentTarget.value)}
defaultValue={stateOrganisasi.create.form.telepon}
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
/>
</Box>
<Box>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
defaultValue={stateOrganisasi.pegawai.create.form.alamat}
onChange={(e) => (stateOrganisasi.pegawai.create.form.alamat = e.currentTarget.value)}
defaultValue={stateOrganisasi.create.form.alamat}
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Posisi
</Text>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={stateOrganisasi.posisiOrganisasi.findMany.data?.map(p => ({
data={stateStrukturBumDes.posisiOrganisasi.findManyAll.data?.map(p => ({
value: p.id,
label: p.nama
})) || []}
defaultValue={stateOrganisasi.pegawai.create.form.posisiId}
value={stateOrganisasi.create.form.posisiId}
onChange={(value) => {
if (value) stateOrganisasi.pegawai.create.form.posisiId = value;
if (value) stateOrganisasi.create.form.posisiId = value;
}}
searchable
clearable
/>
</Box>
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
color="blue"
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)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePegawai;
export default CreatePegawaiBumDes;

View File

@@ -1,33 +1,33 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function Pegawai() {
function PegawaiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pegawai'
title='Pegawai BUMDesa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPegawai search={search} />
<ListPegawaiBumdes search={search} />
</Box>
);
}
function ListPegawai({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
function ListPegawaiBumdes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const router = useRouter();
const {
@@ -39,21 +39,10 @@ function ListPegawai({ search }: { search: string }) {
} = stateOrganisasi.findMany;
useEffect(() => {
load(page, 10);
}, [page]);
load(page, 10, search);
}, [page, search]);
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.namaLengkap?.toLowerCase().includes(keyword) ||
item.gelarAkademik?.toLowerCase().includes(keyword) ||
item.telepon?.toLowerCase().includes(keyword) ||
item.posisi?.nama?.toLowerCase().includes(keyword)
);
});
}, [data, search]);
const filteredData = data || []
// Handle loading state
if (loading || !data) {
@@ -67,29 +56,51 @@ function ListPegawai({ search }: { search: string }) {
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" ta="center">
<p>Tidak ada data pegawai yang tersedia</p>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai BUMDesa</Title>
<Tooltip label="Tambah Pegawai Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Center py="xl">
<Text c="dimmed">Tidak ada data pegawai yang ditemukan</Text>
</Center>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
<JudulList
title='List Pegawai'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create'
/>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai BUMDesa</Title>
<Tooltip label="Tambah Pegawai Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
<TableTh style={{ width: '25%' }}>Nama Lengkap</TableTh>
<TableTh style={{ width: '20%' }}>Posisi</TableTh>
<TableTh style={{ width: '10%' }}>Status</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -106,10 +117,20 @@ function ListPegawai({ search }: { search: string }) {
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}>
<TableTd>{item.namaLengkap}</TableTd>
<TableTd>{item.gelarAkademik}</TableTd>
<TableTd>{item.telepon}</TableTd>
<TableTd>{item.posisi?.nama}</TableTd>
<TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.namaLengkap}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Badge variant="light" color="blue">
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</Box>
</TableTd>
<TableTd>
<Group gap="xs" wrap="nowrap">
<Box visibleFrom="sm">
@@ -131,8 +152,15 @@ function ListPegawai({ search }: { search: string }) {
</Group>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
@@ -140,8 +168,7 @@ function ListPegawai({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
@@ -149,12 +176,14 @@ function ListPegawai({ search }: { search: string }) {
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
</Box>
);
}
export default Pegawai;
export default PegawaiBumDes;

View File

@@ -1,30 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPosisiOrganisasi() {
function EditPosisiOrganisasiBumDes() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
const [formData, setFormData] = useState({
nama: '',
@@ -32,13 +24,17 @@ function EditPosisiOrganisasi() {
hierarki: 0,
});
// Fungsi generik untuk update formData
const handleChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
useEffect(() => {
const loadPosisiOrganisasi = async () => {
if (!id) return;
const loadPosisiOrganisasi = async () => {
try {
const data = await stateOrganisasi.edit.load(id);
if (data) {
stateOrganisasi.edit.id = id;
setFormData({
@@ -47,8 +43,8 @@ function EditPosisiOrganisasi() {
hierarki: data.hierarki || 0,
});
}
} catch (error) {
console.error('Error loading posisi organisasi:', error);
} catch (err) {
console.error('Error loading posisi organisasi:', err);
toast.error('Gagal memuat data posisi organisasi');
}
};
@@ -57,12 +53,13 @@ function EditPosisiOrganisasi() {
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama posisi organisasi tidak boleh kosong');
return;
}
try {
// Update global state hanya saat submit
stateOrganisasi.edit.form = {
nama: formData.nama.trim(),
deskripsi: formData.deskripsi.trim(),
@@ -70,19 +67,17 @@ function EditPosisiOrganisasi() {
};
if (!stateOrganisasi.edit.id) {
stateOrganisasi.edit.id = id; // fallback
stateOrganisasi.edit.id = id;
}
const success = await stateOrganisasi.edit.update();
if (success) {
toast.success('Posisi organisasi berhasil diperbarui!');
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'
);
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi');
}
} catch (error) {
console.error('Error updating posisi organisasi:', error);
} catch (err) {
console.error('Error updating posisi organisasi:', err);
// toast error biasanya sudah ada di update
}
};
@@ -90,17 +85,12 @@ function EditPosisiOrganisasi() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Posisi Organisasi
Edit Posisi Organisasi BUMDes
</Title>
</Group>
@@ -114,44 +104,40 @@ function EditPosisiOrganisasi() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.nama}
onChange={(e) =>
setFormData({ ...formData, nama: e.target.value })
}
label="Nama Posisi Organisasi"
placeholder="Masukkan nama posisi organisasi"
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
<Box>
<Text fz="sm" mb={6}>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData({ ...formData, deskripsi: htmlContent })
}
onChange={(html) => handleChange('deskripsi', html)}
/>
</Box>
<TextInput
type="number"
defaultValue={formData.hierarki}
onChange={(e) =>
setFormData({
...formData,
hierarki: parseInt(e.target.value) || 0,
})
}
label="Hierarki"
placeholder="Masukkan hierarki"
type="number"
min={0}
placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
value={formData.hierarki}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
handleChange('hierarki', isNaN(value) ? 0 : value);
}}
required
/>
<Group justify="right">
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={stateOrganisasi.edit.loading}
radius="md"
size="md"
style={{
@@ -169,4 +155,4 @@ function EditPosisiOrganisasi() {
);
}
export default EditPosisiOrganisasi;
export default EditPosisiOrganisasiBumDes;

View File

@@ -1,68 +1,66 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePosisiOrganisasi() {
function CreatePosisiOrganisasiBumDes() {
const router = useRouter();
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
useEffect(() => {
stateOrganisasi.findMany.load();
}, []);
const resetForm = () => {
// Initialize form with default values
stateOrganisasi.create.form = {
nama: '',
deskripsi: '',
nama: "",
deskripsi: "",
hierarki: 0,
};
return () => {
// Clean up form on unmount
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0,
};
};
}, []);
const handleSubmit = async () => {
try {
if (!stateOrganisasi.create.form.nama.trim()) {
return toast.error('Nama posisi tidak boleh kosong');
}
await stateOrganisasi.create.submit();
resetForm();
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'
);
toast.success('Posisi organisasi berhasil ditambahkan');
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi');
} catch (error) {
toast.error('Gagal menambahkan posisi organisasi');
console.error('Error:', error);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header Back + Title */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Posisi Organisasi
Tambah Posisi Organisasi BUMDes
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
@@ -76,12 +74,14 @@ function CreatePosisiOrganisasi() {
label="Nama Posisi"
placeholder="Contoh: Kepala Desa"
defaultValue={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
onChange={(e) => (stateOrganisasi.create.form.nama = e.target.value)}
required
/>
<Box>
<Text fz={"sm"} mb={6}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateOrganisasi.create.form.deskripsi}
onChange={(htmlContent) => {
@@ -93,21 +93,20 @@ function CreatePosisiOrganisasi() {
<TextInput
label="Hierarki"
type="number"
placeholder="Contoh: 1"
defaultValue={stateOrganisasi.create.form.hierarki}
min={0}
placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
defaultValue={stateOrganisasi.create.form.hierarki || ''}
onChange={(e) => {
const value = parseInt(e.currentTarget.value, 10);
if (!isNaN(value)) {
stateOrganisasi.create.form.hierarki = value;
}
const value = parseInt(e.target.value, 10);
stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value;
}}
required
/>
{/* Action Button */}
<Group justify="right">
<Group justify="flex-end" mt="md">
<Button
onClick={handleSubmit}
loading={stateOrganisasi.create.loading}
radius="md"
size="md"
style={{
@@ -125,4 +124,4 @@ function CreatePosisiOrganisasi() {
);
}
export default CreatePosisiOrganisasi;
export default CreatePosisiOrganisasiBumDes;

View File

@@ -1,58 +1,37 @@
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Group,
Paper,
Pagination,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function PosisiOrganisasi() {
function PosisiOrganisasiBumDes() {
const [search, setSearch] = useState("");
return (
<Box>
{/* Search Bar */}
<HeaderSearch
title="Posisi Organisasi"
placeholder="Cari nama, deskripsi, atau hierarki..."
title='Posisi Organisasi BUMDes'
placeholder='Cari posisi organisasi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
{/* List Table */}
<ListPosisiOrganisasi search={search} />
<ListPosisiOrganisasiBumDes search={search} />
</Box>
);
}
function ListPosisiOrganisasi({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const {
data,
@@ -66,21 +45,20 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
load(page, 10, search);
}, [page, search]);
const filteredData = data || [];
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
load(page, 10, search); // refresh
setModalHapus(false)
setSelectedId(null)
}
};
}
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
<Skeleton height={600} radius="md" />
</Stack>
);
}
@@ -89,63 +67,62 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Posisi Organisasi</Title>
<Tooltip label="Tambah Posisi Baru" withArrow>
<Title order={4}>Daftar Posisi Organisasi BumDes</Title>
<Tooltip label="Tambah Posisi Organisasi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create'
)
}
onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '15%' }}>Hierarki</TableTh>
<TableTh style={{ width: '25%' }}>Edit</TableTh>
<TableTh style={{ width: '25%' }}>Hapus</TableTh>
<TableTh style={{ width: '20%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '20%' }}>Hierarki</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<TableTd style={{ width: '20%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Text fz="sm">{item.hierarki ?? '-'}</Text>
<TableTd style={{ width: '20%' }}>
<Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Tooltip label="Ubah Posisi Organisasi" withArrow>
<TableTd style={{ width: '20%' }}>
<Text>{item.hierarki || '-'}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`
)
}
size="sm"
onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Tooltip label="Hapus Posisi Organisasi" withArrow>
<TableTd style={{ width: '20%' }}>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
@@ -170,8 +147,6 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
</Table>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -186,16 +161,15 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
radius="md"
/>
</Center>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posisi organisasi ini?"
text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?"
/>
</Box>
);
}
export default PosisiOrganisasi;
export default PosisiOrganisasiBumDes;

View File

@@ -0,0 +1,133 @@
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { Box, Center, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core';
import { IconUsers } from '@tabler/icons-react';
import { OrganizationChart } from 'primereact/organizationchart';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function StrukturOrganisasiBumDes() {
return (
<Box py="md">
<ListStrukturOrganisasiBumDes />
</Box>
);
}
function ListStrukturOrganisasiBumDes() {
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
useEffect(() => {
stateOrganisasi.findMany.load();
}, []);
if (stateOrganisasi.findMany.loading) {
return (
<Center py={40}>
<Loader size="lg" />
</Center>
);
}
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
return (
<Stack align="center" py={60} gap="sm">
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" />
<Text fw={500} c="dimmed">Belum ada struktur organisasi yang ditambahkan</Text>
</Stack>
);
}
const posisiMap = new Map<string, any>();
const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive);
for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id;
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: [],
});
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
}
let root: any[] = [];
posisiMap.forEach((posisi) => {
if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId);
if (parent) {
parent.children.push(posisi);
}
} else {
root.push(posisi);
}
});
function toOrgChartFormat(node: any): any {
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ada pegawai',
status: node.nama,
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
},
children: node.children.map(toOrgChartFormat),
};
}
const chartData = root.map(toOrgChartFormat);
return (
<Box py={10}>
<Paper
p="md"
radius="lg"
shadow="md"
withBorder
style={{
overflowX: 'auto',
background: 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)',
}}
>
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
</Paper>
</Box>
);
}
function nodeTemplate(node: any) {
const imageSrc = node?.data?.image || '/img/default.png';
const name = node?.data?.name || 'Tanpa Nama';
const status = node?.data?.status || 'Tidak ada deskripsi';
return (
<Stack align="center" gap={6} p="sm" style={{ minWidth: 160 }}>
<Tooltip label={name} position="top" withArrow>
<Image
src={imageSrc}
alt={name}
radius="xl"
w={100}
h={100}
fit="cover"
style={{
border: '1px solid #D3D1D1FF',
}}
loading='lazy'
/>
</Tooltip>
<Text fw={600} ta="center" size="sm">{name}</Text>
<Text size="xs" c="dimmed" ta="center">{status}</Text>
</Stack>
);
}
export default StrukturOrganisasiBumDes;

View File

@@ -4,7 +4,6 @@ import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
@@ -16,11 +15,10 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch, IconPlus } from '@tabler/icons-react';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -72,20 +70,7 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Ide Inovatif</Title>
<Tooltip label="Ajukan Ide Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/inovasi/ajukan-ide-inovatif/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>

View File

@@ -1,10 +1,21 @@
'use client'
'use client';
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -12,20 +23,22 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPenghargaan() {
const stateDesaDigital = useProxy(desaDigitalState)
const router = useRouter()
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
function EditDigitalSmartVillage() {
const stateDesaDigital = useProxy(desaDigitalState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: stateDesaDigital.findUnique.data?.name || '',
deskripsi: stateDesaDigital.findUnique.data?.deskripsi || '',
imageId: stateDesaDigital.findUnique.data?.imageId || '',
})
name: '',
deskripsi: '',
imageId: '',
});
useEffect(() => {
const loadPenghargaan = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
@@ -38,136 +51,160 @@ function EditPenghargaan() {
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
if (data?.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
console.error("Error loading desa digital smart village:", error);
toast.error("Gagal memuat data desa digital smart village");
console.error('Error loading data:', error);
toast.error('Gagal memuat data desa digital smart village');
}
};
loadPenghargaan();
loadData();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateDesaDigital.edit.form = {
...stateDesaDigital.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData };
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
if (!uploaded?.id) return toast.error('Gagal upload gambar');
stateDesaDigital.edit.form.imageId = uploaded.id;
}
await stateDesaDigital.edit.update();
toast.success("Desa digital smart village berhasil diperbarui!");
router.push("/admin/inovasi/desa-digital-smart-village");
toast.success('Desa digital smart village berhasil diperbarui!');
router.push('/admin/inovasi/desa-digital-smart-village');
} catch (error) {
console.error("Error updating desa digital smart village:", error);
toast.error("Terjadi kesalahan saat memperbarui desa digital smart village");
}
console.error('Error updating desa digital:', error);
toast.error('Terjadi kesalahan saat memperbarui data');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Desa Digital Smart Village</Title>
<TextInput
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Desa Digital Smart Village
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '55%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Dropzone Upload */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Desa Digital
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
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={220} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</div>
</Stack>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview"
alt="Preview Gambar"
radius="md"
style={{
maxWidth: '100%',
maxHeight: '200px',
maxHeight: 220,
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
</Box>
{/* Input Judul */}
<TextInput
label="Judul"
placeholder="Masukkan judul inovasi"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
{/* Editor Deskripsi */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateDesaDigital.edit.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
{/* Tombol Simpan */}
<Group justify="right">
<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)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPenghargaan;
export default EditDigitalSmartVillage;

View File

@@ -1,8 +1,18 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -10,95 +20,136 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
function DetailDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState)
const stateDesaDigital = useProxy(desaDigitalState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
stateDesaDigital.findUnique.load(params?.id as string)
}, [])
stateDesaDigital.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
stateDesaDigital.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/desa-digital-smart-village")
}
stateDesaDigital.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/desa-digital-smart-village");
}
};
if (!stateDesaDigital.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = stateDesaDigital.findUnique.data;
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Desa Digital Smart Village</Text>
{stateDesaDigital.findUnique.data ? (
<Paper key={stateDesaDigital.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{stateDesaDigital.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateDesaDigital.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateDesaDigital.findUnique.data?.image?.link} alt="gambar" loading="lazy"/>
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol Kembali */}
<Button
onClick={() => {
if (stateDesaDigital.findUnique.data) {
setSelectedId(stateDesaDigital.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDesaDigital.delete.loading || !stateDesaDigital.findUnique.data}
color={"red"}
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
<IconX size={20} />
Kembali
</Button>
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Desa Digital Smart Village
</Text>
{/* Sub Card Detail */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Desa Digital'}
w={200}
h={200}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Tombol Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateDesaDigital.findUnique.data) {
router.push(`/admin/inovasi/desa-digital-smart-village/${stateDesaDigital.findUnique.data.id}/edit`);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={!stateDesaDigital.findUnique.data}
color={"green"}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Tooltip>
</Group>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
{/* Modal Konfirmasi */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus desa digital smart village ini?'
text="Apakah Anda yakin ingin menghapus desa digital smart village ini?"
/>
</Box>
);

View File

@@ -22,7 +22,7 @@ import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
import { Dropzone } from '@mantine/dropzone';
function CreateDesaDigital() {
export default function CreateDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
@@ -44,7 +44,6 @@ function CreateDesaDigital() {
}
try {
// Upload gambar dulu
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
@@ -55,10 +54,8 @@ function CreateDesaDigital() {
return toast.error('Gagal mengunggah gambar');
}
// Set imageId ke form
stateDesaDigital.create.form.imageId = uploaded.id;
// Submit form
const success = await stateDesaDigital.create.create();
if (success) {
resetForm();
@@ -72,10 +69,16 @@ function CreateDesaDigital() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
{/* Header dengan tombol kembali */}
<Group mb="md" align="center">
<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"
style={{ transition: 'background 0.2s ease' }}
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -84,28 +87,32 @@ function CreateDesaDigital() {
</Title>
</Group>
{/* Card */}
{/* Card Form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
p={{ base: 'md', md: 'xl' }}
radius="lg"
shadow="md"
style={{
border: '1px solid #eaeaea',
transition: 'box-shadow 0.3s ease',
}}
>
<Stack gap="md">
{/* Nama */}
<Stack gap="lg">
{/* Input Nama */}
<TextInput
label="Nama Desa Digital Smart Village"
placeholder="Masukkan nama desa digital smart village"
label="Nama Desa Digital"
placeholder="Masukkan nama desa digital"
defaultValue={stateDesaDigital.create.form.name}
onChange={(e) => (stateDesaDigital.create.form.name = e.target.value)}
required
radius="md"
withAsterisk
/>
{/* Deskripsi */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
<Text fw={500} fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
@@ -118,8 +125,8 @@ function CreateDesaDigital() {
{/* Upload Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar
<Text fw={500} fz="sm" mb={6}>
Gambar Desa Digital
</Text>
<Dropzone
onDrop={(files) => {
@@ -134,6 +141,11 @@ function CreateDesaDigital() {
accept={{ 'image/*': [] }}
radius="md"
p="xl"
style={{
border: '2px dashed #cfd8dc',
backgroundColor: '#fafafa',
transition: 'background-color 0.2s ease, border-color 0.2s ease',
}}
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
@@ -153,15 +165,22 @@ function CreateDesaDigital() {
{/* Preview */}
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Box
mt="sm"
style={{
textAlign: 'center',
borderRadius: 12,
overflow: 'hidden',
}}
>
<Image
src={previewImage}
alt="Preview"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
maxHeight: 220,
objectFit: 'cover',
border: '1px solid #e0e0e0',
}}
loading="lazy"
/>
@@ -170,7 +189,7 @@ function CreateDesaDigital() {
</Box>
{/* Tombol Submit */}
<Group justify="right">
<Group justify="flex-end" mt="sm">
<Button
onClick={handleSubmit}
radius="md"
@@ -179,6 +198,17 @@ function CreateDesaDigital() {
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
}}
onMouseEnter={(e) => {
(e.currentTarget.style.transform = 'translateY(-2px)');
(e.currentTarget.style.boxShadow =
'0 6px 20px rgba(79, 172, 254, 0.5)');
}}
onMouseLeave={(e) => {
(e.currentTarget.style.transform = 'translateY(0)');
(e.currentTarget.style.boxShadow =
'0 4px 15px rgba(79, 172, 254, 0.4)');
}}
>
Simpan
@@ -189,5 +219,3 @@ function CreateDesaDigital() {
</Box>
);
}
export default CreateDesaDigital;

View File

@@ -97,17 +97,21 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text
fz="sm"
c="dimmed"
lineClamp={2}
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</TableTd>
<TableTd>
<Button

View File

@@ -30,17 +30,19 @@ function EditInfoTeknologiTepatGuna() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: stateInfoTekno.findUnique.data?.name || '',
deskripsi: stateInfoTekno.findUnique.data?.deskripsi || '',
imageId: stateInfoTekno.findUnique.data?.imageId || '',
name: '',
deskripsi: '',
imageId: '',
});
// Load data pertama kali
useEffect(() => {
const id = params?.id as string;
if (!id) return;
const loadPenghargaan = async () => {
const loadData = async () => {
try {
const data = await stateInfoTekno.edit.load(id);
if (data) {
@@ -58,16 +60,19 @@ function EditInfoTeknologiTepatGuna() {
}
};
loadPenghargaan();
loadData();
}, [params?.id]);
// Submit form
const handleSubmit = async () => {
try {
// sync local → global pas submit
stateInfoTekno.edit.form = {
...stateInfoTekno.edit.form,
...formData,
};
// upload file kalau ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -119,8 +124,8 @@ function EditInfoTeknologiTepatGuna() {
<TextInput
label="Judul"
placeholder="Masukkan judul info teknologi tepat guna"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required
/>
@@ -188,10 +193,9 @@ function EditInfoTeknologiTepatGuna() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateInfoTekno.edit.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>

View File

@@ -55,7 +55,7 @@ function DetailInfoTeknologiTepatGuna() {
<Paper
withBorder
w={{ base: "100%", md: "70%", lg: "60%" }}
bg={colors['white-1']}
bg="#ECEEF8"
p="lg"
radius="md"
shadow="sm"

View File

@@ -27,14 +27,14 @@ function EditKolaborasiInovasi() {
const params = useParams();
const [formData, setFormData] = useState({
name: kolaborasiState.update.form.name || "",
deskripsi: kolaborasiState.update.form.deskripsi || "",
tahun: kolaborasiState.update.form.tahun || "",
slug: kolaborasiState.update.form.slug || "",
kolaborator: kolaborasiState.update.form.kolaborator || "",
name: "",
deskripsi: "",
tahun: "",
slug: "",
kolaborator: "",
});
// Load data
// Load data awal dari server
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
@@ -44,11 +44,11 @@ function EditKolaborasiInovasi() {
const data = await kolaborasiState.update.load(id);
if (data) {
setFormData({
name: data.name || "",
deskripsi: data.deskripsi || "",
tahun: data.tahun || "",
slug: data.slug || "",
kolaborator: data.kolaborator || "",
name: data.name ?? "",
deskripsi: data.deskripsi ?? "",
tahun: data.tahun?.toString() ?? "",
slug: data.slug ?? "",
kolaborator: data.kolaborator ?? "",
});
}
} catch (error) {
@@ -60,6 +60,7 @@ function EditKolaborasiInovasi() {
loadKolaborasi();
}, [params?.id]);
// Handler submit → baru update global state
const handleSubmit = async () => {
try {
kolaborasiState.update.form = {
@@ -70,6 +71,7 @@ function EditKolaborasiInovasi() {
slug: formData.slug,
kolaborator: formData.kolaborator,
};
await kolaborasiState.update.submit();
toast.success("Kolaborasi inovasi berhasil diperbarui!");
router.push("/admin/inovasi/kolaborasi-inovasi");
@@ -79,6 +81,11 @@ function EditKolaborasiInovasi() {
}
};
// Handler input (biar lebih DRY)
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
return (
<Box px={{ base: "sm", md: "lg" }} py="md">
<Group mb="md">
@@ -104,32 +111,32 @@ function EditKolaborasiInovasi() {
<TextInput
label="Nama Kolaborasi"
placeholder="Masukkan nama kolaborasi"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
defaultValue={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
value={formData.slug}
onChange={(e) => handleChange("slug", e.target.value)}
required
/>
<TextInput
label="Tahun"
placeholder="Masukkan tahun"
defaultValue={formData.tahun}
onChange={(e) => setFormData({ ...formData, tahun: e.target.value })}
value={formData.tahun}
onChange={(e) => handleChange("tahun", e.target.value)}
required
/>
<TextInput
label="Kolaborator"
placeholder="Masukkan nama kolaborator"
defaultValue={formData.kolaborator}
onChange={(e) => setFormData({ ...formData, kolaborator: e.target.value })}
value={formData.kolaborator}
onChange={(e) => handleChange("kolaborator", e.target.value)}
required
/>
@@ -139,10 +146,7 @@ function EditKolaborasiInovasi() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
kolaborasiState.update.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) => handleChange("deskripsi", htmlContent)}
/>
</Box>

View File

@@ -33,17 +33,22 @@ function EditMitraKolaborasi() {
const state = useProxy(mitraKolaborasi);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// Local form state (controlled)
const [formData, setFormData] = useState({
name: state.update.form.name || '',
imageId: state.update.form.imageId || '',
name: '',
imageId: '',
});
// Load data ke state lokal sekali saja
useEffect(() => {
const loadFoto = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
@@ -51,25 +56,31 @@ function EditMitraKolaborasi() {
name: data.name || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error('Error loading foto:', error);
toast.error('Gagal memuat data foto');
console.error('Error loading data:', error);
toast.error('Gagal memuat data mitra');
}
};
loadFoto();
loadData();
}, [params?.id]);
const handleChange = (key: string, value: string) => {
setFormData((prev) => ({
...prev,
[key]: value,
}));
};
const handleSubmit = async () => {
try {
state.update.form = {
...state.update.form,
name: formData.name,
imageId: formData.imageId,
};
// upload file jika ada
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -79,8 +90,16 @@ function EditMitraKolaborasi() {
if (!uploaded?.id) {
return toast.error('Gagal upload gambar');
}
state.update.form.imageId = uploaded.id;
imageId = uploaded.id;
}
// update global state hanya saat submit
state.update.form = {
...state.update.form,
name: formData.name,
imageId,
};
await state.update.update();
toast.success('Mitra berhasil diperbarui!');
router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
@@ -118,8 +137,8 @@ function EditMitraKolaborasi() {
<TextInput
label="Nama Mitra"
placeholder="Masukkan nama mitra"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
required
/>

View File

@@ -1,9 +1,19 @@
'use client'
'use client';
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -11,31 +21,34 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditJenisLayanan() {
const state = useProxy(layananonlineDesa.jenisLayanan)
const router = useRouter()
const params = useParams()
const state = useProxy(layananonlineDesa.jenisLayanan);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
nama: state.edit.form.nama,
deskripsi: state.edit.form.deskripsi,
})
nama: '',
deskripsi: '',
});
useEffect(() => {
const loadJenisLayanan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.edit.load(id);
if (data) {
setFormData({
nama: data.nama,
deskripsi: data.deskripsi,
nama: data.nama ?? '',
deskripsi: data.deskripsi ?? '',
});
}
} catch (error) {
console.error("Error loading jenis layanan:", error);
toast.error("Gagal memuat data jenis layanan");
console.error('Error loading jenis layanan:', error);
toast.error('Gagal memuat data jenis layanan');
}
};
loadJenisLayanan();
}, [params?.id]);
@@ -43,46 +56,89 @@ function EditJenisLayanan() {
try {
state.edit.form = {
...state.edit.form,
nama: formData.nama,
deskripsi: formData.deskripsi,
}
await state.edit.update()
toast.success("Jenis layanan berhasil diperbarui!")
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
...formData,
};
await state.edit.update();
toast.success('Jenis layanan berhasil diperbarui!');
router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
} catch (error) {
console.error("Error updating jenis layanan:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis layanan");
}
console.error('Error updating jenis layanan:', error);
toast.error('Terjadi kesalahan saat memperbarui jenis layanan');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Jenis Layanan</Title>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jenis Layanan
</Title>
</Group>
{/* Form Container */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input: Nama Jenis Layanan */}
<TextInput
defaultValue={formData.nama}
onChange={(val) => {
setFormData({ ...formData, nama: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Jenis Layanan</Text>}
placeholder="masukkan nama jenis layanan"
label="Nama Jenis Layanan"
placeholder="Masukkan nama jenis layanan"
value={formData.nama}
onChange={(e) =>
setFormData((prev) => ({ ...prev, nama: e.target.value }))
}
required
/>
{/* Input: Deskripsi (Rich Text Editor) */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
onChange={(htmlContent) =>
setFormData((prev) => ({
...prev,
deskripsi: htmlContent,
}))
}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol Submit */}
<Group justify="right" mt="sm">
<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)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

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