Compare commits

...

29 Commits

Author SHA1 Message Date
9622eb5a9a Fix QC Kak Inno Admin, Fix QC Keano UI User, Fix QC Pak jun tabel apbdes 2025-11-12 17:42:31 +08:00
417a8937f5 Semua tooltips di admin sudah dihilangkan 2025-11-07 14:38:32 +08:00
db8909b9ed Fix Text to Speech Menu Landing Page && Add barchart Landing Page APBDes 2025-11-06 11:35:04 +08:00
f66a46f645 QC ToolTip Admin Keano Masih di Menu Landing Page - Keamanan, QC Dari Darmasaba Pop Up Notifikasi 2025-11-05 14:32:38 +08:00
fb57698dc9 Add Menu Musik
Add News Reader for Difable
Add Running text news / announcement
2025-11-04 15:08:48 +08:00
d128313e71 Fix QC Keano FrontEnd
Fix QC Kak Ayu Admin 29 Okt
2025-11-03 17:36:00 +08:00
7b4bb1e58e QC Kak Inno FrontEnd Done
QC Kak Ayu FrontEnd Done
QC Keano 31 Okt
2025-11-03 10:28:03 +08:00
0befe6a3f2 QC Kak Inno 28 Okt
QC Kak Ayu 28 Okt
QC Keano 28 Okt
2025-10-30 15:51:12 +08:00
a6663bbcee QC Kak Inno 27 Oct
QC Kak Ayu 27 Oct
QC Keano 27 Oct
QC Pak Jun 27 Oct
2025-10-28 17:34:38 +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
768 changed files with 32723 additions and 15994 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,9 +3,9 @@
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "bun --bun next dev --hostname 0.0.0.0",
"build": "bun --bun next build",
"start": "bun --bun next start"
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"prisma": {
"seed": "bun run prisma/seed.ts"
@@ -19,6 +19,7 @@
"@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
"@emotion/react": "^11.14.0",
"@mantine/carousel": "^7.16.2",
"@mantine/charts": "^7.17.1",
"@mantine/core": "^7.17.4",
@@ -26,6 +27,7 @@
"@mantine/dropzone": "^8.1.1",
"@mantine/form": "^8.1.0",
"@mantine/hooks": "^7.17.4",
"@mantine/modals": "^8.3.6",
"@mantine/tiptap": "^7.17.4",
"@paljs/types": "^8.1.0",
"@prisma/client": "^6.3.1",
@@ -43,6 +45,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,9 +55,11 @@
"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",
"embla-carousel": "^8.6.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"extract-zip": "^2.0.1",
"form-data": "^4.0.2",
"framer-motion": "^12.23.5",
@@ -71,17 +76,20 @@
"next": "^15.5.2",
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
"nodemailer": "^7.0.10",
"p-limit": "^6.2.0",
"primeicons": "^7.0.0",
"primereact": "^10.9.6",
"prisma": "^6.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-exif-orientation-img": "^0.1.5",
"react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"sharp": "^0.34.3",

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
@@ -286,49 +287,51 @@ model StrukturPPID {
}
model PosisiOrganisasiPPID {
id String @id @default(cuid())
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai PegawaiPPID[]
strukturOrganisasi StrukturPPID[] // Relasi balik
parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent")
id String @id @default(cuid())
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai PegawaiPPID[]
strukturOrganisasi StrukturPPID[] // Relasi balik
parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent")
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
}
model PegawaiPPID {
id String @id @default(cuid())
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturPPID[] // Relasi balik
id String @id @default(cuid())
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturPPID[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
}
model StrukturOrganisasiPPID {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String @db.Uuid
hubunganOrganisasiId String @db.Uuid
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String
hubunganOrganisasiId String
posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id])
pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
isActive Boolean @default(true)
}
// ========================================= VISI MISI PPID ========================================= //
@@ -850,7 +853,7 @@ model JadwalKegiatan {
syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1167,6 +1170,7 @@ model KontakDarurat {
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
whatsapp String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1340,6 +1344,7 @@ model PasarDesa {
harga Int
rating Float
alamatUsaha String
kontak String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1382,6 +1387,7 @@ model LowonganPekerjaan {
gaji String
deskripsi String
kualifikasi String
notelp String
tanggalPosting DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
@@ -1391,79 +1397,67 @@ model LowonganPekerjaan {
// ========================================= STRUKTUR ORGANISASI ========================================= //
model PosisiOrganisasi {
id String @id @default(uuid()) @db.VarChar(50)
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posisi_organisasi")
model StrukturBumDes {
id String @id @default(cuid())
name String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
PosisiOrganisasiBumDes PosisiOrganisasiBumDes? @relation(fields: [posisiOrganisasiBumDesId], references: [id])
posisiOrganisasiBumDesId String?
PegawaiBumDes PegawaiBumDes? @relation(fields: [pegawaiBumDesId], references: [id])
pegawaiBumDesId String?
}
model Pegawai {
id String @id @default(uuid()) @db.Uuid
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasi @relation(fields: [posisiId], references: [id])
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@map("pegawai")
model PosisiOrganisasiBumDes {
id String @id @default(cuid())
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai PegawaiBumDes[]
strukturOrganisasi StrukturBumDes[] // Relasi balik
parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiBumDes? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiBumDes[] @relation("Parent")
StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
}
model HubunganOrganisasi {
id String @id @default(uuid()) @db.Uuid
atasanId String @db.Uuid
bawahanId String @db.Uuid
tipe String? @db.VarChar(50)
atasan Pegawai @relation("AtasanToBawahan", fields: [atasanId], references: [id])
bawahan Pegawai @relation("BawahanToAtasan", fields: [bawahanId], references: [id])
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
@@unique([atasanId, bawahanId])
@@map("hubungan_organisasi")
model PegawaiBumDes {
id String @id @default(cuid())
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasiBumDes @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturBumDes[] // Relasi balik
StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
}
model StrukturOrganisasi {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String @db.Uuid
hubunganOrganisasiId String @db.Uuid
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
hubunganOrganisasi HubunganOrganisasi @relation(fields: [hubunganOrganisasiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
@@map("struktur_organisasi")
model StrukturOrganisasiBumDes {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String
hubunganOrganisasiId String
posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id])
pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
}
// ========================================= PROGRAM KEMISKINAN ========================================= //
@@ -1612,7 +1606,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
}
// ========================================= INOVASI ========================================= //
// ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital {
id String @id @default(cuid())
@@ -2093,6 +2087,9 @@ model DataPerpustakaan {
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
// relasi baru ke peminjaman
peminjamanBuku PeminjamanBuku[]
}
model KategoriBuku {
@@ -2105,6 +2102,31 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[]
}
model PeminjamanBuku {
id String @id @default(cuid())
nama String
noTelp String
alamat String
bukuId String
tanggalPinjam DateTime @default(now())
batasKembali DateTime // tenggat waktu pengembalian
tanggalKembali DateTime? // diisi saat dikembalikan
status StatusPeminjaman @default(Dipinjam)
catatan String? // opsional, misal: kondisi buku
buku DataPerpustakaan @relation(fields: [bukuId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
enum StatusPeminjaman {
Dipinjam
Dikembalikan
Terlambat
Dibatalkan
}
// ========================================= USER ========================================= //
model User {

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();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

After

Width:  |  Height:  |  Size: 378 KiB

View File

@@ -7,6 +7,7 @@ import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { useEffect } from 'react';
type CreateEditorProps = {
value: string;
@@ -32,6 +33,13 @@ export default function CreateEditor({ value, onChange }: CreateEditorProps) {
},
});
// 👇 Tambahkan efek untuk sinkronisasi value dari luar (resetForm)
useEffect(() => {
if (editor && value !== editor.getHTML()) {
editor.commands.setContent(value || '');
}
}, [value, editor]);
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">

View File

@@ -47,6 +47,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) {
editor.off('update', updateHandler);
};
}, [editor, onChange]);
return (
<RichTextEditor editor={editor}>

View File

@@ -1,6 +1,6 @@
'use client'
'use client';
import { Box, rem, Select } from '@mantine/core';
import { Box, Group, rem, Select, SelectProps } from '@mantine/core';
import {
IconAmbulance,
IconCash,
@@ -25,7 +25,7 @@ import {
IconTrophy,
IconTruckFilled,
IconBuilding,
IconAlertTriangle
IconAlertTriangle,
} from '@tabler/icons-react';
const iconMap = {
@@ -38,26 +38,26 @@ const iconMap = {
scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco},
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled},
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp},
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet},
bantuan: {label: 'Bantuan', icon: IconCash},
pelatihan: {label: 'Pelatihan', icon: IconSchool},
subsidi: {label: 'Subsidi', icon: IconShoppingCart},
layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital},
polisi: {label: 'Polisi', icon: IconShieldFilled},
ambulans: {label: 'Ambulans', icon: IconAmbulance},
pemadam: {label: 'Pemadam', icon: IconFiretruck},
rumahSakit: {label: 'Rumah Sakit', icon: IconHospital},
bangunan: {label: 'Bangunan', icon: IconBuilding},
darurat: {label: 'Darurat', icon: IconAlertTriangle},
lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
rumah: { label: 'Rumah', icon: IconHome },
pohon: { label: 'Pohon', icon: IconTree },
air: { label: 'Air', icon: IconDroplet },
bantuan: { label: 'Bantuan', icon: IconCash },
pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
polisi: { label: 'Polisi', icon: IconShieldFilled },
ambulans: { label: 'Ambulans', icon: IconAmbulance },
pemadam: { label: 'Pemadam', icon: IconFiretruck },
rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
bangunan: { label: 'Bangunan', icon: IconBuilding },
darurat: { label: 'Darurat', icon: IconAlertTriangle },
};
type IconKey = keyof typeof iconMap;
export type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({
value,
@@ -67,44 +67,52 @@ const iconList = Object.entries(iconMap).map(([value, data]) => ({
export default function SelectIconProgramEdit({
onChange,
value,
...props
}: {
onChange: (value: IconKey) => void;
value: IconKey;
}) {
const IconComponent = iconMap[value]?.icon || null;
onChange: (value: IconKey | '') => void;
value: IconKey | '';
} & Omit<SelectProps, 'onChange' | 'value' | 'data'>) {
return (
<Box maw={300}>
<Select
placeholder="Pilih ikon"
value={value}
onChange={(value) => {
if (value) onChange(value as IconKey);
value={value || ''}
onChange={(val: string | null) => {
if (val) {
onChange(val as IconKey);
} else {
onChange('');
}
}}
data={iconList}
renderOption={({ option }) => {
const Icon = iconMap[option.value as IconKey]?.icon;
return (
<Group gap="sm">
{Icon && <Icon size={18} stroke={1.5} />}
{option.label}
</Group>
);
}}
leftSection={
IconComponent && (
<Box>
<IconComponent size={24} stroke={1.5} />
value && iconMap[value as IconKey] ? (
<Box ml={-4}>
{(() => {
const Icon = iconMap[value as IconKey].icon;
return <Icon size={20} stroke={1.5} />;
})()}
</Box>
)
) : null
}
withCheckIcon={false}
searchable={false}
rightSectionWidth={0}
searchable
styles={{
input: {
textAlign: 'left',
fontSize: rem(16),
paddingLeft: 40,
},
section: {
left: 10,
right: 'auto',
fontSize: rem(16),
},
}}
{...props}
/>
</Box>
);
}
}

View File

@@ -75,17 +75,18 @@ const berita = proxy({
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path
const startTime = Date.now();
berita.findMany.loading = true;
berita.findMany.page = page;
berita.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -98,9 +99,16 @@ const berita = proxy({
berita.findMany.data = [];
berita.findMany.totalPages = 1;
} finally {
berita.findMany.loading = false;
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
berita.findMany.loading = false;
}, delay);
}
},
},
},
findUnique: {

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

@@ -39,7 +39,7 @@ const penghargaanState = proxy({
);
if (res.status === 200) {
penghargaanState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -287,7 +287,7 @@ const pengumuman = proxy({
);
if (res.status === 200) {
pengumuman.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -101,6 +101,38 @@ const ApbDesa = proxy({
}
},
},
findFirst: {
data: null as Prisma.ApbDesaGetPayload<{
include: { pendapatan: true; belanja: true; pembiayaan: true };
}> | null,
loading: false,
async load(params?: Record<string, any>) {
try {
this.loading = true;
// ✅ request ke endpoint find-first
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
"find-first"
].get({ query: params || {} });
if (res.status === 200 && res.data?.success) {
this.data = res.data.data ?? null;
} else {
this.data = null;
toast.error(res.data?.message || "Gagal memuat data pertama APB Desa");
}
} catch (error) {
console.error("Error findFirst APB Desa:", error);
toast.error("Gagal memuat data APB Desa pertama");
this.data = null;
} finally {
this.loading = false;
}
},
reset() {
this.data = null;
},
},
update: {
id: "",
form: { ...ApbDesaDefaultForm },

View File

@@ -49,7 +49,7 @@ const demografiPekerjaan = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
demografiPekerjaan.create.form = { ...defaultForm };
demografiPekerjaan.findMany.load();
return id;

View File

@@ -47,7 +47,7 @@ const jumlahPendudukMiskin = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
jumlahPendudukMiskin.create.form = {
year: 0,
totalPoorPopulation: 0,

View File

@@ -89,7 +89,7 @@ const jumlahPengangguran = proxy({
if (res.status === 200) {
const id = res.data?.id;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
jumlahPengangguran.findMany.load();
return id;

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({
@@ -45,7 +47,7 @@ const lowonganKerjaState = proxy({
);
if (res.status === 200) {
lowonganKerjaState.create.loading = false;
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
@@ -179,6 +181,7 @@ const lowonganKerjaState = proxy({
gaji: data.gaji,
deskripsi: data.deskripsi,
kualifikasi: data.kualifikasi,
notelp: data.notelp,
};
return data;
} else {
@@ -218,6 +221,7 @@ const lowonganKerjaState = proxy({
gaji: this.form.gaji,
deskripsi: this.form.deskripsi,
kualifikasi: this.form.kualifikasi,
notelp: this.form.notelp,
}),
});
if (!response.ok) {

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

@@ -45,7 +45,7 @@ const programKemiskinanState = proxy({
);
if (res.status === 200) {
programKemiskinanState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -46,7 +46,7 @@ const grafikSektorUnggulan = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
grafikSektorUnggulan.create.form = {
name: "",
description: "",

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

View File

@@ -51,7 +51,7 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
grafikBerdasarkanUsiaKerjaNganggur.create.form = {
usia18_25: "",
usia26_35: "",
@@ -255,7 +255,7 @@ const grafikBerdasarkanPendidikan = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
grafikBerdasarkanPendidikan.create.form = {
SD: "",
SMP: "",

View File

@@ -37,7 +37,7 @@ const desaDigitalState = proxy({
);
if (res.status === 200) {
desaDigitalState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -37,7 +37,7 @@ const infoTeknoState = proxy({
);
if (res.status === 200) {
infoTeknoState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -6,9 +6,9 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
name: z.string().min(5, "Nama minimal 5 karakter"),
deskripsi: z.string().min(5, "Deskripsi minimal 5 karakter"),
slug: z.string().min(5, "Deskripsi singkat minimal 5 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
@@ -29,26 +29,33 @@ const programKreatifState = proxy({
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
toast.error(err);
return false; // ⬅️ ini penting
}
try {
programKreatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
programKreatifState.create.form
);
if (res.status === 200) {
programKreatifState.findMany.load();
return toast.success("success create");
toast.success("Sukses menambahkan");
return true;
}
console.log(res);
return toast.error("failed create");
toast.error("failed create");
return false;
} catch (error) {
console.log((error as Error).message);
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat create");
return false;
} finally {
programKreatifState.create.loading = false;
}
},
}
},
findMany: {
data: null as any[] | null,

View File

@@ -37,7 +37,7 @@ const keamananLingkunganState = proxy({
].post(keamananLingkunganState.create.form);
if (res.status === 200) {
keamananLingkunganState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -38,7 +38,7 @@ const kontakDaruratKeamananState = proxy({
].post(kontakDaruratKeamananState.create.form);
if (res.status === 200) {
kontakDaruratKeamananState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
@@ -294,7 +294,7 @@ const kontakDaruratItem = proxy({
);
if (res.status === 200) {
kontakDaruratItem.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -88,7 +88,7 @@ const laporanPublikState = proxy({
if (res.status === 200) {
laporanPublikState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);

View File

@@ -40,7 +40,7 @@ const pencegahanKriminalitasState = proxy({
].post(pencegahanKriminalitasState.create.form);
if (res.status === 200) {
pencegahanKriminalitasState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -37,7 +37,7 @@ const tipsKeamananState = proxy({
);
if (res.status === 200) {
tipsKeamananState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -351,7 +351,7 @@ const dokter = proxy({
if (res.status === 200) {
const id = res.data?.data;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
dokter.create.create.form = { ...defaultDokterForm };
dokter.findMany.load();
return id;

View File

@@ -43,7 +43,7 @@ const grafikkepuasan = proxy({
if (res.status === 200) {
const id = res.data?.data;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
grafikkepuasan.create.form = { ...defaultForm };
grafikkepuasan.findMany.load();
return id;

View File

@@ -50,7 +50,7 @@ const persentasekelahiran = proxy({
if (res.status === 200) {
const id = res.data?.data;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
persentasekelahiran.create.form = { ...defaultForm };
persentasekelahiran.findMany.load();
return id;

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

@@ -53,7 +53,7 @@ const programInovasi = proxy({
].post(formData);
if (res.status === 200) {
programInovasi.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
@@ -474,7 +474,7 @@ const mediaSosial = proxy({
);
if (res.status === 200) {
mediaSosial.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -93,6 +93,34 @@ const sdgsDesa = proxy({
}
},
},
findManyAll: {
data: null as any[] | null,
loading: false,
load: async () => { // Change to arrow function
sdgsDesa.findManyAll.loading = true; // Use the full path to access the property
try {
const query: any = {};
const res = await ApiFetch.api.landingpage.sdgsdesa[
"findManyAll"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
sdgsDesa.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load media sosial:", res.data?.message);
sdgsDesa.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading media sosial:", error);
sdgsDesa.findManyAll.data = [];
} finally {
sdgsDesa.findManyAll.loading = false;
}
},
},
findUnique: {
data: null as Prisma.SdgsDesaGetPayload<{
include: {

View File

@@ -39,7 +39,7 @@ const dataLingkunganDesaState = proxy({
);
if (res.status === 200) {
dataLingkunganDesaState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -35,7 +35,7 @@ const pengelolaanSampah = proxy({
].post(pengelolaanSampah.create.form);
if (res.status === 200) {
pengelolaanSampah.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

View File

@@ -39,7 +39,7 @@ const programPenghijauanState = proxy({
);
if (res.status === 200) {
programPenghijauanState.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");

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

@@ -42,7 +42,7 @@ const dataPendidikan = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
dataPendidikan.create.form = {
name: "",
jumlah: "",

View File

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

View File

@@ -38,7 +38,7 @@ const daftarInformasiPublik = proxy({
].post(daftarInformasiPublik.create.form);
if (res.status === 200) {
daftarInformasiPublik.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
return toast.error("failed create");
} catch (error) {

View File

@@ -41,7 +41,7 @@ const grafikBerdasarkanUmur = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
toast.success("Sukses menambahkan");
grafikBerdasarkanUmur.create.form = {
remaja: "",
dewasa: "",

View File

@@ -88,7 +88,7 @@ const statepermohonanInformasiPublik = proxy({
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
if (res.status === 200) {
statepermohonanInformasiPublik.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
return toast.error("failed create");
} catch (error) {

View File

@@ -37,7 +37,7 @@ const permohonanKeberatanInformasi = proxy({
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
if (res.status === 200) {
permohonanKeberatanInformasi.findMany.load();
return toast.success("success create");
return toast.success("Sukses menambahkan");
}
return toast.error("failed create");
} catch (error) {

View File

@@ -3,9 +3,6 @@ import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
/**
* Schema validasi form ProfilePPID menggunakan Zod.
*/
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
biodata: z.string().min(3, "Biodata minimal 3 karakter"),
@@ -33,25 +30,16 @@ type ProfilePPIDForm = Prisma.ProfilePPIDGetPayload<{
pengalaman: true;
unggulan: true;
imageId: true;
image?: {
select: {
link: true;
};
};
image?: { select: { link: true } };
};
}>;
/**
* Improved State Management - Consolidated and more robust
*/
const stateProfilePPID = proxy({
// Consolidated data management
profile: {
data: null as ProfilePPIDForm | null,
loading: false,
error: null as string | null,
// Single method to load profile data
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
@@ -62,52 +50,42 @@ const stateProfilePPID = proxy({
this.error = null;
try {
const response = await fetch(`/api/ppid/profileppid/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const res = await fetch(`/api/ppid/profileppid/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const result = await response.json();
const result = await res.json();
if (result.success) {
this.data = result.data;
return result.data;
} else {
throw new Error(result.message || "Gagal mengambil data profile");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load profile error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data profile");
} else throw new Error(result.message || "Gagal memuat data profile");
} catch (err) {
const msg = (err as Error).message;
this.error = msg;
console.error("Load profile error:", msg);
toast.error("Gagal memuat data profile");
return null;
} finally {
this.loading = false;
}
},
// Reset profile data
reset() {
this.data = null;
this.error = null;
this.loading = false;
}
},
},
// Edit form management
editForm: {
id: "",
form: { ...defaultForm },
originalForm: { ...defaultForm }, // ✅ Tambah field originalForm
loading: false,
error: null as string | null,
isReadOnly: false, // Flag untuk data yang tidak bisa diedit
// Initialize form with profile data
initialize(profileData: ProfilePPIDForm) {
this.id = profileData.id;
this.isReadOnly = false; // Semua data bisa diedit
this.form = {
const data = {
name: profileData.name || "",
biodata: profileData.biodata || "",
riwayat: profileData.riwayat || "",
@@ -115,23 +93,20 @@ const stateProfilePPID = proxy({
unggulan: profileData.unggulan || "",
imageId: profileData.imageId || "",
};
this.form = { ...data };
this.originalForm = { ...data }; // ✅ Simpan versi original
},
// Update form field
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
// Submit form
async submit() {
// Validate form
const validation = templateForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
const check = templateForm.safeParse(this.form);
if (!check.success) {
toast.error(
check.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
);
return false;
}
@@ -139,63 +114,54 @@ const stateProfilePPID = proxy({
this.error = null;
try {
const response = await fetch(`/api/ppid/profileppid/${this.id}`, {
const res = await fetch(`/api/ppid/profileppid/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const result = await res.json();
if (result.success) {
toast.success("Berhasil update profile");
// Refresh profile data
await stateProfilePPID.profile.load(this.id);
this.originalForm = { ...this.form }; // ✅ Update original setelah sukses
return true;
} else {
throw new Error(result.message || "Gagal update profile");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update profile error:", errorMessage);
toast.error("Terjadi kesalahan saat update profile");
} else throw new Error(result.message || "Gagal update profile");
} catch (err) {
const msg = (err as Error).message;
this.error = msg;
toast.error(msg);
return false;
} finally {
this.loading = false;
}
},
// Reset form
// ✅ Tambahan reset ke original data
resetToOriginal() {
this.form = { ...this.originalForm };
toast.info("Data dikembalikan ke kondisi awal");
},
reset() {
this.id = "";
this.form = { ...defaultForm };
this.originalForm = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
}
},
},
// Helper methods
async loadForEdit(id: string) {
const profileData = await this.profile.load(id);
if (profileData) {
this.editForm.initialize(profileData);
}
return profileData;
const data = await this.profile.load(id);
if (data) this.editForm.initialize(data);
return data;
},
reset() {
this.profile.reset();
this.editForm.reset();
}
},
});
export default stateProfilePPID;
export default stateProfilePPID;

View File

@@ -561,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,

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconBuildingStore, IconFileText, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -14,36 +14,31 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
label: "Pelayanan Surat Keterangan",
value: "pelayanansuratketerangan",
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
icon: <IconFileText size={18} stroke={1.8} />,
tooltip: "Layanan terkait surat keterangan resmi desa"
icon: <IconFileText size={18} stroke={1.8} />
},
{
label: "Pelayanan Perizinan Berusaha",
value: "pelayananperizinanusaha",
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
icon: <IconBuildingStore size={18} stroke={1.8} />,
tooltip: "Layanan untuk izin usaha masyarakat"
icon: <IconBuildingStore size={18} stroke={1.8} />
},
{
label: "Pelayanan Telunjuk Sakti Desa",
value: "pelayanantelunjuksaktidesa",
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
icon: <IconSparkles size={18} stroke={1.8} />,
tooltip: "Layanan inovasi khusus desa"
icon: <IconSparkles size={18} stroke={1.8} />
},
{
label: "Pelayanan Penduduk Non-Permanent",
value: "pelayanannonpermanent",
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Pendataan penduduk non-permanent"
icon: <IconUsers size={18} stroke={1.8} />
},
{
label: "Ajukan Permohonan",
value: "ajukanpermohonan",
href: "/admin/desa/layanan/ajukan_permohonan",
icon: <IconUsersPlus size={18} stroke={1.8} />,
tooltip: "Ajukan permohonan"
icon: <IconUsersPlus size={18} stroke={1.8} />
}
];
@@ -91,14 +86,8 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
@@ -109,7 +98,6 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconCategory, IconNews } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconNews, IconCategory } from '@tabler/icons-react';
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -15,15 +15,13 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
label: "List Berita",
value: "list_berita",
href: "/admin/desa/berita/list-berita",
icon: <IconNews size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola semua berita desa"
icon: <IconNews size={18} stroke={1.8} />
},
{
label: "Kategori Berita",
value: "kategori_berita",
href: "/admin/desa/berita/kategori-berita",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori berita desa"
icon: <IconCategory size={18} stroke={1.8} />
},
];
@@ -71,46 +69,39 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
<Tooltip
<TabsTab
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel>
))}
</Tabs>
</Stack>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel>
))}
</Tabs>
</Stack >
);
}

View File

@@ -11,7 +11,7 @@ import {
Stack,
TextInput,
Title,
Tooltip,
Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -23,6 +23,11 @@ function EditKategoriBerita() {
const editState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter();
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: '',
});
const [formData, setFormData] = useState({
name: '',
@@ -39,6 +44,9 @@ function EditKategoriBerita() {
setFormData({
name: data.name || '',
});
setOriginalData({
name: data.name || '',
});
}
} catch (error) {
console.error('Error loading kategori Berita:', error);
@@ -56,8 +64,16 @@ function EditKategoriBerita() {
}));
};
const handleResetForm = () => {
setFormData({
name: originalData.name,
});
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// update global state hanya saat submit
editState.update.form = {
...editState.update.form,
@@ -70,6 +86,8 @@ function EditKategoriBerita() {
} catch (error) {
console.error('Error updating kategori Berita:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
} finally {
setIsSubmitting(false);
}
};
@@ -77,7 +95,6 @@ function EditKategoriBerita() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button + Title */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
@@ -86,7 +103,6 @@ function EditKategoriBerita() {
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Berita
</Title>
@@ -112,6 +128,17 @@ function EditKategoriBerita() {
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -122,7 +149,7 @@ function EditKategoriBerita() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -9,15 +9,18 @@ import {
Stack,
TextInput,
Title,
Tooltip
Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateKategoriBerita() {
const createState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
createState.create.form = {
@@ -26,16 +29,23 @@ function CreateKategoriBerita() {
};
const handleSubmit = async () => {
await createState.create.create();
resetForm();
router.push('/admin/desa/berita/kategori-berita');
setIsSubmitting(true);
try {
await createState.create.create();
resetForm();
router.push('/admin/desa/berita/kategori-berita');
} catch (error) {
console.error('Error creating kategori berita:', error);
toast.error('Gagal menambahkan kategori berita');
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
@@ -44,7 +54,6 @@ function CreateKategoriBerita() {
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Berita
</Title>
@@ -63,12 +72,23 @@ function CreateKategoriBerita() {
<TextInput
label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita"
defaultValue={createState.create.form.name || ''}
value={createState.create.form.name || ''}
onChange={(e) => (createState.create.form.name = e.target.value)}
required
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -79,7 +99,7 @@ function CreateKategoriBerita() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -17,8 +17,7 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip
Title
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -86,7 +85,6 @@ function ListKategoriBerita({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kategori Berita</Title>
<Tooltip label="Tambah Kategori Berita" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -97,7 +95,6 @@ function ListKategoriBerita({ search }: { search: string }) {
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
@@ -123,7 +120,6 @@ function ListKategoriBerita({ search }: { search: string }) {
</Text>
</TableTd>
<TableTd>
<Tooltip label="Edit Kategori Berita" withArrow>
<Button
variant="light"
color="green"
@@ -135,10 +131,8 @@ function ListKategoriBerita({ search }: { search: string }) {
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus Kategori Berita" withArrow>
<Button
variant="light"
color="red"
@@ -150,7 +144,6 @@ function ListKategoriBerita({ search }: { search: string }) {
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))

View File

@@ -6,6 +6,7 @@ import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
ActionIcon,
Box,
Button,
Group,
@@ -16,7 +17,7 @@ import {
Text,
TextInput,
Title,
Tooltip,
Loader
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import {
@@ -45,6 +46,17 @@ function EditBerita() {
imageId: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
judul: "",
deskripsi: "",
kategoriBeritaId: "",
content: "",
imageId: "",
imageUrl: ""
});
// Load kategori + berita
useEffect(() => {
beritaState.kategoriBerita.findMany.load();
@@ -64,6 +76,15 @@ function EditBerita() {
imageId: data.imageId || "",
});
setOriginalData({
judul: data.judul || "",
deskripsi: data.deskripsi || "",
kategoriBeritaId: data.kategoriBeritaId || "",
content: data.content || "",
imageId: data.imageId || "",
imageUrl: data.image?.link || ""
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
@@ -83,6 +104,7 @@ function EditBerita() {
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// Update global state hanya sekali di sini
beritaState.berita.edit.form = {
...beritaState.berita.edit.form,
@@ -109,23 +131,36 @@ function EditBerita() {
} catch (error) {
console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita");
} finally {
setIsSubmitting(false);
}
};
const handleResetForm = () => {
setFormData({
judul: originalData.judul,
deskripsi: originalData.deskripsi,
kategoriBeritaId: originalData.kategoriBeritaId,
content: originalData.content,
imageId: originalData.imageId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
return (
<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>
</Tooltip>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Berita
</Title>
@@ -219,14 +254,14 @@ function EditBerita() {
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
@@ -238,6 +273,24 @@ function EditBerita() {
}}
loading="lazy"
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
@@ -257,17 +310,29 @@ function EditBerita() {
{/* Action */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
color: "#fff",
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -1,14 +1,14 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
function DetailBerita() {
const beritaState = useProxy(stateDashboardBerita);
@@ -111,7 +111,6 @@ function DetailBerita() {
{/* Action Button */}
<Group gap="sm">
<Tooltip label="Hapus Berita" withArrow position="top">
<Button
color="red"
onClick={() => {
@@ -124,9 +123,7 @@ function DetailBerita() {
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Berita" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
@@ -136,7 +133,6 @@ function DetailBerita() {
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>

View File

@@ -14,7 +14,8 @@ import {
Text,
TextInput,
Title,
Tooltip,
Loader,
ActionIcon
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks';
@@ -29,6 +30,7 @@ export default function CreateBerita() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
useShallowEffect(() => {
beritaState.kategoriBerita.findMany.load();
@@ -47,42 +49,48 @@ export default function CreateBerita() {
};
const handleSubmit = async () => {
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
try {
setIsSubmitting(true);
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
beritaState.berita.create.form.imageId = uploaded.id;
await beritaState.berita.create.create();
resetForm();
router.push('/admin/desa/berita/list-berita');
} catch (error) {
console.error('Error creating berita:', error);
toast.error('Terjadi kesalahan saat membuat berita');
} finally {
setIsSubmitting(false);
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
beritaState.berita.create.form.imageId = uploaded.id;
await beritaState.berita.create.create();
resetForm();
router.push('/admin/desa/berita/list-berita');
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol kembali */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Berita
</Title>
@@ -100,7 +108,7 @@ export default function CreateBerita() {
<TextInput
label="Judul"
placeholder="Masukkan judul berita"
defaultValue={beritaState.berita.create.form.judul}
value={beritaState.berita.create.form.judul}
onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
required
/>
@@ -112,7 +120,7 @@ export default function CreateBerita() {
label: item.name,
value: item.id,
}))}
defaultValue={beritaState.berita.create.form.kategoriBeritaId || null}
value={beritaState.berita.create.form.kategoriBeritaId || null}
onChange={(val: string | null) => {
if (val) {
const selected = beritaState.kategoriBerita.findMany.data?.find(
@@ -157,7 +165,7 @@ export default function CreateBerita() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -178,7 +186,7 @@ export default function CreateBerita() {
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
@@ -190,6 +198,26 @@ export default function CreateBerita() {
}}
loading="lazy"
/>
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
@@ -207,6 +235,17 @@ export default function CreateBerita() {
</Box>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -217,7 +256,7 @@ export default function CreateBerita() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
@@ -68,7 +67,6 @@ function ListBerita({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Berita</Title>
<Tooltip label="Tambah Berita" withArrow>
<Button
leftSection={<IconCircleDashedPlus size={18} />}
color="blue"
@@ -77,7 +75,6 @@ function ListBerita({ search }: { search: string }) {
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>

View File

@@ -13,8 +13,7 @@ import {
Stack,
Text,
TextInput,
Title,
Tooltip,
Title
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
@@ -103,7 +102,6 @@ export default function ListImage() {
</Box>
<Group justify="space-between" align="center" pt="xs">
<Tooltip label="Hapus foto" withArrow>
<ActionIcon
variant="subtle"
color="red"
@@ -116,7 +114,6 @@ export default function ListImage() {
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</Group>
</Stack>
</Card>

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconPhoto, IconVideo } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconPhoto, IconVideo } from '@tabler/icons-react';
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -14,15 +14,13 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
label: "Foto",
value: "foto",
href: "/admin/desa/gallery/foto",
icon: <IconPhoto size={18} stroke={1.8} />,
tooltip: "Kelola foto-foto galeri desa"
icon: <IconPhoto size={18} stroke={1.8} />
},
{
label: "Video",
value: "video",
href: "/admin/desa/gallery/video",
icon: <IconVideo size={18} stroke={1.8} />,
tooltip: "Kelola video galeri desa"
icon: <IconVideo size={18} stroke={1.8} />
},
];
@@ -70,25 +68,18 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
<Tooltip
<TabsTab
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>

View File

@@ -4,6 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Group,
@@ -11,11 +12,11 @@ import {
Stack,
TextInput,
Title,
Tooltip
Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState, useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
@@ -25,6 +26,14 @@ function EditVideo() {
const videoState = useProxy(stateGallery.video);
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
linkVideo: "",
});
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
@@ -45,6 +54,11 @@ function EditVideo() {
deskripsi: data.deskripsi ?? '',
linkVideo: data.linkVideo ?? '',
});
setOriginalData({
name: data.name ?? '',
deskripsi: data.deskripsi ?? '',
linkVideo: data.linkVideo ?? '',
});
}
} catch (error) {
console.error('Error loading video:', error);
@@ -62,25 +76,42 @@ function EditVideo() {
[]
);
const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
linkVideo: originalData.linkVideo,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
videoState.update.form = {
name: formData.name,
deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo,
};
await videoState.update.update();
toast.success('Video berhasil diperbarui!');
router.push('/admin/desa/gallery/video');
setIsSubmitting(true);
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
try {
videoState.update.form = {
name: formData.name,
deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo,
};
await videoState.update.update();
toast.success('Video berhasil diperbarui!');
router.push('/admin/desa/gallery/video');
} catch (error) {
console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video');
}
} catch (error) {
console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video');
} finally {
setIsSubmitting(false);
}
};
@@ -89,16 +120,14 @@ function EditVideo() {
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"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Video
</Title>
@@ -130,7 +159,7 @@ function EditVideo() {
required
/>
{embedLink && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Box mt="sm" pos="relative" style={{ display: 'flex', justifyContent: 'center' }}>
<iframe
className="rounded"
width="100%"
@@ -138,7 +167,27 @@ function EditVideo() {
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setFormData({
...formData,
linkVideo: '',
});
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
@@ -154,6 +203,17 @@ function EditVideo() {
</Box>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -164,7 +224,7 @@ function EditVideo() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -111,7 +111,6 @@ function DetailVideo() {
{/* Tombol Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Video" withArrow position="top">
<Button
color="red"
onClick={() => {
@@ -124,9 +123,7 @@ function DetailVideo() {
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Video" withArrow position="top">
<Button
color="green"
onClick={() =>
@@ -138,7 +135,6 @@ function DetailVideo() {
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>

View File

@@ -3,6 +3,7 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Group,
@@ -11,9 +12,9 @@ import {
Text,
TextInput,
Title,
Tooltip,
Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
@@ -25,6 +26,7 @@ function CreateVideo() {
const router = useRouter();
const [link, setLink] = useState('');
const embedLink = convertYoutubeUrlToEmbed(link);
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
videoState.create.form = {
@@ -36,31 +38,37 @@ function CreateVideo() {
};
const handleSubmit = async () => {
if (!embedLink) {
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return;
}
try {
setIsSubmitting(true);
if (!embedLink) {
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return;
}
videoState.create.form.linkVideo = embedLink;
await videoState.create.create();
resetForm();
router.push('/admin/desa/gallery/video');
videoState.create.form.linkVideo = embedLink;
await videoState.create.create();
resetForm();
router.push('/admin/desa/gallery/video');
} catch (error) {
console.error("Error creating video:", error);
toast.error("Terjadi kesalahan saat menambahkan video");
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header Back Button + Title */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Video
</Title>
@@ -80,7 +88,7 @@ function CreateVideo() {
<TextInput
label="Judul Video"
placeholder="Masukkan judul video"
defaultValue={videoState.create.form.name}
value={videoState.create.form.name}
onChange={(e) => {
videoState.create.form.name = e.currentTarget.value;
}}
@@ -91,14 +99,14 @@ function CreateVideo() {
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
defaultValue={link}
value={link}
onChange={(e) => setLink(e.currentTarget.value)}
required
/>
{/* Preview Video */}
{embedLink && (
<Box mt="sm">
<Box mt="sm" pos="relative">
<iframe
style={{
borderRadius: 10,
@@ -109,7 +117,24 @@ function CreateVideo() {
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setLink('');
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
@@ -128,6 +153,17 @@ function CreateVideo() {
{/* Button Submit */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -138,7 +174,7 @@ function CreateVideo() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -74,7 +73,6 @@ function ListVideo({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Video</Title>
<Tooltip label="Tambah Video Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -83,7 +81,6 @@ function ListVideo({ search }: { search: string }) {
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>

View File

@@ -6,12 +6,12 @@ import {
Box,
Button,
Group,
Loader,
Paper,
Select,
Stack,
TextInput,
Title,
Tooltip,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -24,6 +24,16 @@ function EditAjukanPermohonan() {
const params = useParams();
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
nama: "",
nik: "",
alamat: "",
nomorKk: "",
kategoriId: "",
});
// State lokal form
const [formData, setFormData] = useState({
nama: '',
@@ -51,6 +61,13 @@ function EditAjukanPermohonan() {
nomorKk: data.nomorKk || '',
kategoriId: data.kategoriId || '',
});
setOriginalData({
nama: data.nama || '',
nik: data.nik || '',
alamat: data.alamat || '',
nomorKk: data.nomorKk || '',
kategoriId: data.kategoriId || '',
});
}
} catch (error) {
console.error('Error loading ajukan:', error);
@@ -69,8 +86,20 @@ function EditAjukanPermohonan() {
}));
};
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
nik: originalData.nik,
alamat: originalData.alamat,
nomorKk: originalData.nomorKk,
kategoriId: originalData.kategoriId,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
stateAjukan.edit.form = {
...stateAjukan.edit.form,
...formData,
@@ -80,6 +109,8 @@ function EditAjukanPermohonan() {
} catch (error) {
console.error('Error updating ajukan:', error);
toast.error('Terjadi kesalahan saat memperbarui ajukan');
} finally {
setIsSubmitting(false);
}
};
@@ -87,11 +118,9 @@ function EditAjukanPermohonan() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Ajukan Permohonan
</Title>
@@ -156,6 +185,17 @@ function EditAjukanPermohonan() {
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -166,7 +206,7 @@ function EditAjukanPermohonan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -9,8 +9,7 @@ import {
Paper,
Skeleton,
Stack,
Text,
Tooltip
Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -121,7 +120,6 @@ function DetailAjukanPermohonan() {
</Box>
<Group gap="sm">
<Tooltip label="Hapus Surat" withArrow position="top">
<Button
color="red"
onClick={() => {
@@ -135,9 +133,7 @@ function DetailAjukanPermohonan() {
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Surat" withArrow position="top">
<Button
color="green"
onClick={() =>
@@ -151,7 +147,6 @@ function DetailAjukanPermohonan() {
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>

View File

@@ -8,12 +8,12 @@ import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -33,6 +33,14 @@ function EditPelayananPendudukNonPermanent() {
deskripsi: '',
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: '',
deskripsi: '',
});
// Load data sekali dari backend
useEffect(() => {
const loadData = async () => {
@@ -46,6 +54,10 @@ function EditPelayananPendudukNonPermanent() {
name: data.name || '',
deskripsi: data.deskripsi || '',
});
setOriginalData({
name: data.name || '',
deskripsi: data.deskripsi || '',
});
}
} catch (error) {
console.error('Error loading data:', error);
@@ -58,41 +70,55 @@ function EditPelayananPendudukNonPermanent() {
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
if (!statePendudukNonPermanent.findById.data) return;
try {
setIsSubmitting(true);
if (!statePendudukNonPermanent.findById.data) return;
// Update global state hanya di submit
const updated = {
...statePendudukNonPermanent.findById.data,
name: formData.name,
deskripsi: formData.deskripsi,
};
// 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');
await statePendudukNonPermanent.update.update(updated);
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memuat data pelayanan penduduk non permanent');
} finally {
setIsSubmitting(false);
}
};
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Pelayanan Penduduk Non Permanent
</Title>
@@ -130,25 +156,31 @@ function EditPelayananPendudukNonPermanent() {
</Box>
{/* Submit Button */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePendudukNonPermanent.update.loading}
disabled={!formData.name}
>
{statePendudukNonPermanent.update.loading
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button>
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
onClick={() => router.back()}
disabled={statePendudukNonPermanent.update.loading}
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -11,8 +11,7 @@ import {
Skeleton,
Stack,
Text,
Title,
Tooltip,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
@@ -27,7 +26,7 @@ function PelayananPendudukNonPermanent() {
);
useShallowEffect(() => {
pelayananPendudukNonPermanen.findById.load('1');
pelayananPendudukNonPermanen.findById.load('edit');
}, []);
if (!pelayananPendudukNonPermanen.findById.data) {
@@ -51,7 +50,6 @@ function PelayananPendudukNonPermanent() {
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Tooltip label="Edit Data Pelayanan" withArrow>
<Button
c="green"
variant="light"
@@ -59,13 +57,12 @@ function PelayananPendudukNonPermanent() {
radius="md"
onClick={() =>
router.push(
'/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit'
`/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
)
}
>
Edit
</Button>
</Tooltip>
</GridCol>
</Grid>

View File

@@ -0,0 +1,211 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Skeleton,
Stack,
TextInput,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPelayananPerizinanBerusaha() {
const router = useRouter();
const params = useParams<{ id: string }>();
const id = params?.id; // ini langsung string
const state = useProxy(stateLayananDesa.pelayananPerizinanBerusaha);
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState({
id: '',
name: '',
deskripsi: '',
link: '',
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
id: '',
name: '',
deskripsi: '',
link: '',
});
// Load data detail
useEffect(() => {
if (!id) {
toast.error("ID tidak valid");
return;
}
const loadData = async () => {
try {
setLoading(true);
const data = await state.findById.load(id);
if (data) {
setFormData({
id: data.id,
name: data.name || "",
deskripsi: data.deskripsi || "",
link: data.link || "",
});
setOriginalData({
id: data.id,
name: data.name || "",
deskripsi: data.deskripsi || "",
link: data.link || "",
});
} else {
toast.error("Data tidak ditemukan");
}
} catch (error) {
console.error("Error loading data:", error);
toast.error("Gagal memuat data");
} finally {
setLoading(false);
}
};
loadData();
}, [id]);
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleResetForm = () => {
setFormData({
id: originalData.id,
name: originalData.name,
deskripsi: originalData.deskripsi,
link: originalData.link,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
await state.update.update(formData);
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha');
} catch (error) {
console.error('Error updating pelayanan perizinan berusaha:', error);
toast.error('Terjadi kesalahan saat update data');
} finally {
setIsSubmitting(false);
}
};
if (loading) {
return (
<Stack align="center" justify="center" py="xl">
<Skeleton height={800} radius="md" />
</Stack>
);
}
return (
<Box>
<Stack gap="xs">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Pelayanan Perizinan Berusaha
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs">
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
<TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.name}
onChange={(e) => handleChange('name')(e.target.value)}
required
/>
<TextInput
label="Link"
placeholder="Masukkan link terkait"
value={formData.link}
onChange={(e) => handleChange('link')(e.target.value)}
/>
<Box>
<Title order={6}>Deskripsi</Title>
<EditEditor
value={formData.deskripsi}
onChange={handleChange('deskripsi')}
/>
</Box>
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default EditPelayananPerizinanBerusaha;

View File

@@ -1,166 +0,0 @@
/* 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 { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPelayananPerizinanBerusaha() {
const router = useRouter();
const params = useParams();
const statePerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
link: '',
});
// load data pertama kali
useEffect(() => {
const loadPelayananPerizinan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePerizinanBerusaha.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
link: data.link || '',
});
}
} catch (error) {
console.error('Error loading pelayanan perizinan berusaha:', error);
toast.error('Gagal memuat data pelayanan perizinan berusaha');
}
};
loadPelayananPerizinan();
}, [params?.id]);
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
const { name, deskripsi, link } = formData;
if (statePerizinanBerusaha.findById.data) {
const updatedData = {
...statePerizinanBerusaha.findById.data,
name,
deskripsi,
link,
};
await statePerizinanBerusaha.update.update(updatedData);
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha');
}
};
return (
<Box>
<Stack gap="xs">
{/* Header Section */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pelayanan Perizinan Berusaha
</Title>
</Group>
{/* Form Section */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs">
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
{/* Nama Field */}
<TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.name}
onChange={(e) => handleChange('name')(e.target.value)}
required
/>
{/* Link Field */}
<TextInput
label="Link"
placeholder="Masukkan link terkait"
value={formData.link}
onChange={(e) => handleChange('link')(e.target.value)}
/>
{/* Deskripsi Field */}
<Box>
<Title order={6}>Deskripsi</Title>
<EditEditor
value={formData.deskripsi}
onChange={handleChange('deskripsi')}
/>
</Box>
{/* Action Buttons */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePerizinanBerusaha.update.loading}
disabled={!formData.name}
>
{statePerizinanBerusaha.update.loading
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={() => router.back()}
disabled={statePerizinanBerusaha.update.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default EditPelayananPerizinanBerusaha;

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import {
@@ -15,39 +16,61 @@ import {
StepperCompleted,
StepperStep,
Text,
Title,
Tooltip,
Title
} from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import stateLayananDesa from '../../../_state/desa/layananDesa';
function PerizinanBerusaha() {
const router = useRouter();
const pelayananPerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [active, setActive] = useState(1);
const nextStep = () =>
setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () =>
setActive((current) => (current > 0 ? current - 1 : current));
useShallowEffect(() => {
pelayananPerizinanBerusaha.findById.load('1');
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
// You should get the ID from your router query or params
const id = 'edit'; // Replace with actual ID or get from URL params
await pelayananPerizinanBerusaha.findById.load(id);
} catch (err) {
setError('Gagal memuat data');
console.error('Error:', err);
} finally {
setLoading(false);
}
};
loadData();
}, []);
if (!pelayananPerizinanBerusaha.findById.data) {
if (loading) {
return (
<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 (
@@ -61,7 +84,6 @@ function PerizinanBerusaha() {
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Tooltip label="Edit Data Perizinan" withArrow>
<Button
c="green"
variant="light"
@@ -69,13 +91,12 @@ function PerizinanBerusaha() {
radius="md"
onClick={() =>
router.push(
'/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
`/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
)
}
>
Edit
</Button>
</Tooltip>
</GridCol>
</Grid>
@@ -183,3 +204,4 @@ function PerizinanBerusaha() {
}
export default PerizinanBerusaha;

View File

@@ -1,129 +1,280 @@
'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */
'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 ApiFetch from '@/lib/api-fetch';
import {
ActionIcon,
Box,
Button,
Group,
Image,
Loader,
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';
import { useEffect, useState, useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import * as React from 'react';
// 🔹 Types
interface FormData {
name: string;
deskripsi: string;
imageId: string;
image2Id: string;
imageUrl: string;
image2Url: string;
}
interface FileUploaderProps {
title: string;
file: File | null;
setFile: React.Dispatch<React.SetStateAction<File | null>>;
preview: string | null;
setPreview: React.Dispatch<React.SetStateAction<string | null>>;
}
// 🔹 File Uploader Component
const FileUploader: React.FC<FileUploaderProps> = ({
title,
file,
setFile,
preview,
setPreview
}) => {
const handleDrop = (files: File[]) => {
const selected = files[0];
if (selected) {
setFile(selected);
setPreview(URL.createObjectURL(selected));
}
};
const handleRemove = () => {
setPreview(null);
setFile(null);
};
return (
<Box>
<Text fw="bold" fz="sm" mb={6}>
{title}
</Text>
<Dropzone
onDrop={handleDrop}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format .png, .jpg, .jpeg, .webp
</Text>
</Stack>
</Group>
</Dropzone>
{preview && (
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={preview}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={handleRemove}
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
);
};
// 🔹 Main Component
function EditSuratKeterangan() {
const router = useRouter();
const params = useParams();
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
// state lokal untuk form
const [formData, setFormData] = useState({
// 🧩 State
const [formData, setFormData] = useState<FormData>({
name: '',
deskripsi: '',
imageId: '',
image2Id: '',
imageUrl: '',
image2Url: '',
});
// state file upload
const [originalData, setOriginalData] = useState<FormData>(formData);
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);
const [isSubmitting, setIsSubmitting] = useState(false);
// load data awal
useEffect(() => {
const loadSurat = async () => {
const id = params?.id as string;
if (!id) return;
// 🧭 Load Initial Data
useEffect(() => {
const loadSurat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateSurat.edit.load(id);
if (data) {
// merge style -> isi hanya field kosong
setFormData((prev) => ({
...prev,
name: prev.name || data.name || '',
deskripsi: prev.deskripsi || data.deskripsi || '',
imageId: prev.imageId || data.imageId || '',
image2Id: prev.image2Id || data.image2Id || '',
}));
try {
const data = await stateLayananDesa.suratKeterangan.edit.load(id);
if (!data) return;
if (data.image?.link && !previewImage) {
setPreviewImage(data.image.link);
}
if (data.image2?.link && !previewImage2) {
setPreviewImage2(data.image2.link);
}
const mapped: FormData = {
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
image2Id: data.image2Id || '',
imageUrl: data.image?.link || '',
image2Url: data.image2?.link || ''
};
setFormData(mapped);
setOriginalData(mapped);
if (data.image?.link) setPreviewImage(data.image.link);
if (data.image2?.link) setPreviewImage2(data.image2.link);
} catch (error) {
console.error('Error loading surat:', error);
toast.error('Gagal memuat data surat');
}
};
loadSurat();
}, [params?.id]);
// 📤 Upload File Helper
const uploadFile = async (file: File): Promise<string | null> => {
try {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
return uploaded?.id || null;
} catch (error) {
console.error("Error loading surat:", error);
toast.error("Gagal memuat data surat");
console.error('Error uploading file:', error);
return null;
}
};
loadSurat();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.id]);
// 🔁 Reset Form
const handleResetForm = () => {
setFormData(originalData);
setPreviewImage(originalData.imageUrl || null);
setPreviewImage2(originalData.image2Url || null);
setFile(null);
setFile2(null);
toast.info('Form dikembalikan ke data awal');
};
// handler untuk submit
// 💾 Submit Handler
const handleSubmit = useCallback(async () => {
try {
// update form global hanya saat submit
stateSurat.edit.form = { ...stateSurat.edit.form, ...formData };
setIsSubmitting(true);
// upload file 1
// ✅ Access original state directly (not proxy)
const originalState = stateLayananDesa.suratKeterangan;
// Update form data properties individually
originalState.edit.form.name = formData.name;
originalState.edit.form.deskripsi = formData.deskripsi;
originalState.edit.form.imageId = formData.imageId;
originalState.edit.form.image2Id = formData.image2Id;
// Upload file 1 if exists
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
stateSurat.edit.form.imageId = uploaded.id;
const uploadedId = await uploadFile(file);
if (!uploadedId) {
toast.error('Gagal upload gambar pertama');
return;
}
originalState.edit.form.imageId = uploadedId;
}
// upload file 2
// Upload file 2 if exists
if (file2) {
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
stateSurat.edit.form.image2Id = uploaded.id;
const uploadedId = await uploadFile(file2);
if (!uploadedId) {
toast.error('Gagal upload gambar kedua');
return;
}
originalState.edit.form.image2Id = uploadedId;
}
await stateSurat.edit.update();
// Submit update
await originalState.edit.update();
toast.success('Surat berhasil diperbarui!');
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) {
console.error('Error updating surat:', error);
toast.error('Terjadi kesalahan saat memperbarui surat');
} finally {
setIsSubmitting(false);
}
}, [formData, file, file2, router, stateSurat.edit]);
}, [formData, file, file2, router]);
// 📝 Form Field Handlers
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, name: e.target.value }));
};
const handleDeskripsiChange = (html: string) => {
setFormData(prev => ({ ...prev, deskripsi: html }));
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */}
{/* 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>
</Tooltip>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Surat Keterangan
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
@@ -133,154 +284,66 @@ useEffect(() => {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input nama */}
{/* Nama Surat */}
<TextInput
label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
onChange={handleNameChange}
required
/>
{/* Input deskripsi */}
{/* Deskripsi */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
onChange={handleDeskripsiChange}
/>
</Box>
{/* Upload Gambar 1 */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Konten Pelayanan
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{/* Gambar 1 */}
<FileUploader
title="Gambar Konten Pelayanan"
file={file}
setFile={setFile}
preview={previewImage}
setPreview={setPreviewImage}
/>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar 1"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
{/* Upload Gambar 2 */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Alur Pelayanan Surat
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile2(selectedFile);
setPreviewImage2(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage2 && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage2}
alt="Preview Gambar 2"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
{/* Gambar 2 */}
<FileUploader
title="Gambar Alur Pelayanan Surat"
file={file2}
setFile={setFile2}
preview={previewImage2}
setPreview={setPreviewImage2}
/>
{/* Action Buttons */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
onClick={handleResetForm}
disabled={isSubmitting}
>
Batal
</Button>
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={isSubmitting}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
boxShadow: '0 4px 15px rgba(79,172,254,0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
@@ -289,4 +352,4 @@ useEffect(() => {
);
}
export default EditSuratKeterangan;
export default EditSuratKeterangan;

View File

@@ -10,8 +10,7 @@ import {
Paper,
Skeleton,
Stack,
Text,
Tooltip,
Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -142,7 +141,6 @@ function DetailSuratKeterangan() {
</Box>
<Group gap="sm">
<Tooltip label="Hapus Surat" withArrow position="top">
<Button
color="red"
onClick={() => {
@@ -156,9 +154,7 @@ function DetailSuratKeterangan() {
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Surat" withArrow position="top">
<Button
color="green"
onClick={() =>
@@ -172,7 +168,6 @@ function DetailSuratKeterangan() {
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>

View File

@@ -5,16 +5,17 @@ import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
ActionIcon,
Box,
Button,
Group,
Image,
Loader,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -28,6 +29,7 @@ function CreateSuratKeterangan() {
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateSurat.create.form = {
@@ -46,6 +48,7 @@ function CreateSuratKeterangan() {
}
try {
setIsSubmitting(true);
// Upload gambar utama
const res1 = await ApiFetch.api.fileStorage.create.post({
file: previewImage.file,
@@ -78,6 +81,8 @@ function CreateSuratKeterangan() {
} catch (error) {
console.error('Error creating surat keterangan:', error);
toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
} finally {
setIsSubmitting(false);
}
};
@@ -85,11 +90,9 @@ function CreateSuratKeterangan() {
<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>
</Tooltip>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Surat Keterangan
</Title>
@@ -106,7 +109,7 @@ function CreateSuratKeterangan() {
<Stack gap="md">
{/* Nama Surat */}
<TextInput
defaultValue={stateSurat.create.form.name}
value={stateSurat.create.form.name}
onChange={(val) => (stateSurat.create.form.name = val.target.value)}
label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
@@ -143,7 +146,7 @@ function CreateSuratKeterangan() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -164,7 +167,7 @@ function CreateSuratKeterangan() {
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage.preview}
alt="Preview Gambar Utama"
@@ -172,6 +175,23 @@ function CreateSuratKeterangan() {
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
@@ -193,7 +213,7 @@ function CreateSuratKeterangan() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -214,7 +234,7 @@ function CreateSuratKeterangan() {
</Dropzone>
{previewImage2 ? (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage2.preview}
alt="Preview Gambar Tambahan"
@@ -222,6 +242,23 @@ function CreateSuratKeterangan() {
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage2(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
) : (
<Text size="sm" c="dimmed" mt="sm" ta="center">
@@ -232,6 +269,17 @@ function CreateSuratKeterangan() {
{/* Tombol Simpan */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -242,7 +290,7 @@ function CreateSuratKeterangan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -17,8 +17,7 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip
Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -82,7 +81,6 @@ function ListSuratKeterangan({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Surat Keterangan</Title>
<Tooltip label="Tambah Surat Keterangan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -93,7 +91,6 @@ function ListSuratKeterangan({ search }: { search: string }) {
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>

View File

@@ -6,24 +6,23 @@ import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState, useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter();
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
name: '',
@@ -31,6 +30,12 @@ function EditPelayananTelunjukSakti() {
link: '',
});
const [originalData, setOriginalData] = useState({
name: '',
deskripsi: '',
link: '',
});
// Load data awal hanya sekali (pas ada id)
useEffect(() => {
const loadData = async () => {
@@ -45,6 +50,11 @@ function EditPelayananTelunjukSakti() {
deskripsi: data.deskripsi ?? '',
link: data.link ?? '',
});
setOriginalData({
name: data.name ?? '',
deskripsi: data.deskripsi ?? '',
link: data.link ?? '',
});
}
} catch (error) {
console.error('Error loading pelayanan telunjuk sakti:', error);
@@ -63,9 +73,19 @@ function EditPelayananTelunjukSakti() {
[]
);
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
link: originalData.link,
});
toast.info("Form dikembalikan ke data awal");
};
// Submit: update global state hanya saat simpan
const handleSubmit = async () => {
try {
setIsSubmitting(true);
stateTelunjukDesa.edit.form = {
...stateTelunjukDesa.edit.form,
...formData,
@@ -76,6 +96,8 @@ function EditPelayananTelunjukSakti() {
} catch (error) {
console.error('Error updating pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
} finally {
setIsSubmitting(false);
}
};
@@ -83,11 +105,9 @@ function EditPelayananTelunjukSakti() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button + Title */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pelayanan Telunjuk Sakti Desa
</Title>
@@ -111,27 +131,36 @@ function EditPelayananTelunjukSakti() {
required
/>
{/* Deskripsi pakai editor */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => handleChange('deskripsi', htmlContent)}
/>
</Box>
{/* Deskripsi */}
<TextInput
value={formData.deskripsi}
onChange={(e) => handleChange('deskripsi', e.target.value)}
label="Judul Link"
placeholder="Masukkan judul link"
required
/>
{/* Link */}
<TextInput
label="Link"
placeholder="Masukkan link terkait"
placeholder="Masukkan alamat link"
value={formData.link}
onChange={(e) => handleChange('link', e.target.value)}
/>
{/* Tombol Simpan */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -142,7 +171,7 @@ function EditPelayananTelunjukSakti() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -9,8 +9,7 @@ import {
Paper,
Skeleton,
Stack,
Text,
Tooltip,
Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -130,7 +129,6 @@ function DetailPelayananTelunjukSakti() {
</Box>
<Group gap="sm">
<Tooltip label="Hapus Layanan" withArrow position="top">
<Button
color="red"
onClick={() => {
@@ -144,9 +142,7 @@ function DetailPelayananTelunjukSakti() {
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Layanan" withArrow position="top">
<Button
color="green"
onClick={() =>
@@ -160,7 +156,6 @@ function DetailPelayananTelunjukSakti() {
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>

View File

@@ -6,20 +6,22 @@ import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
TextInput,
Title,
Tooltip
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePelayananTelunjukDesa() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateTelunjukDesa.create.form = {
@@ -31,6 +33,7 @@ function CreatePelayananTelunjukDesa() {
const handleSubmit = async () => {
try {
setIsSubmitting(true);
await stateTelunjukDesa.create.create();
resetForm();
toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan');
@@ -38,6 +41,8 @@ function CreatePelayananTelunjukDesa() {
} catch (error) {
console.error('Error create pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat menambahkan data');
} finally {
setIsSubmitting(false);
}
};
@@ -45,11 +50,9 @@ function CreatePelayananTelunjukDesa() {
<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>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Pelayanan Telunjuk Sakti Desa
</Title>
@@ -67,7 +70,7 @@ function CreatePelayananTelunjukDesa() {
<Stack gap="md">
{/* Nama */}
<TextInput
defaultValue={stateTelunjukDesa.create.form.name}
value={stateTelunjukDesa.create.form.name}
onChange={(val) => {
stateTelunjukDesa.create.form.name = val.target.value;
}}
@@ -78,18 +81,18 @@ function CreatePelayananTelunjukDesa() {
{/* Deskripsi */}
<TextInput
defaultValue={stateTelunjukDesa.create.form.deskripsi}
value={stateTelunjukDesa.create.form.deskripsi}
onChange={(val) => {
stateTelunjukDesa.create.form.deskripsi = val.target.value;
}}
label="Deskripsi"
placeholder="Masukkan deskripsi pelayanan"
label="Judul Link"
placeholder="Masukkan judul link"
required
/>
{/* Link */}
<TextInput
defaultValue={stateTelunjukDesa.create.form.link}
value={stateTelunjukDesa.create.form.link}
onChange={(val) => {
stateTelunjukDesa.create.form.link = val.target.value;
}}
@@ -100,6 +103,17 @@ function CreatePelayananTelunjukDesa() {
{/* Tombol Simpan */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -110,7 +124,7 @@ function CreatePelayananTelunjukDesa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -1,159 +1,3 @@
// /* eslint-disable react-hooks/exhaustive-deps */
// 'use client'
// import colors from '@/con/colors';
// import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
// import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
// import { useRouter } from 'next/navigation';
// import { useEffect, useMemo, useState } from 'react';
// import { useProxy } from 'valtio/utils';
// import HeaderSearch from '../../../_com/header';
// import JudulList from '../../../_com/judulList';
// import stateLayananDesa from '../../../_state/desa/layananDesa';
// function PelayananTelunjukSakti() {
// const [search, setSearch] = useState("");
// return (
// <Box>
// <HeaderSearch
// title='Posisi Organisasi'
// placeholder='pencarian'
// searchIcon={<IconSearch size={20} />}
// value={search}
// onChange={(e) => setSearch(e.currentTarget.value)}
// />
// <ListPelayananTelunjukSakti search={search} />
// </Box>
// );
// }
// function ListPelayananTelunjukSakti({ search }: { search: string }) {
// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
// const router = useRouter()
// const {
// data,
// page,
// totalPages,
// loading,
// load,
// } = telunjukSaktiState.findMany;
// useEffect(() => {
// load(page, 10)
// }, [])
// const filteredData = useMemo(() => {
// if (!data) return [];
// return data.filter(item => {
// const keyword = search.toLowerCase();
// return (
// item.name?.toLowerCase().includes(keyword) ||
// item.link?.toLowerCase().includes(keyword) ||
// item.deskripsi?.toLowerCase().includes(keyword)
// );
// })
// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
// }, [data, search]);
// if (loading || !data) {
// return (
// <Stack py={10}>
// <Skeleton height={300} />
// </Stack>
// );
// }
// if (data.length === 0) {
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <JudulList
// title='List Pelayanan Telunjuk Sakti Desa'
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
// />
// <Table striped withTableBorder withRowBorders>
// <TableThead>
// <TableTr>
// <TableTh>Nama</TableTh>
// <TableTh>Link</TableTh>
// <TableTh>Detail</TableTh>
// </TableTr>
// </TableThead>
// <TableTbody>
// <TableTr>
// <TableTd colSpan={3}>
// <Text fz={"sm"} color="gray.5">
// Tidak ada data
// </Text>
// </TableTd>
// </TableTr>
// </TableTbody>
// </Table>
// </Paper>
// </Box>
// );
// }
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <JudulList
// title='List Pelayanan Telunjuk Sakti Desa'
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
// />
// <Table striped withTableBorder withRowBorders>
// <TableThead>
// <TableTr>
// <TableTh>Nama</TableTh>
// <TableTh>Link</TableTh>
// <TableTh>Detail</TableTh>
// </TableTr>
// </TableThead>
// <TableTbody>
// {filteredData.map((item) => (
// <TableTr key={item.id}>
// <TableTd>
// <Box w={100}>
// <Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
// </Box>
// </TableTd>
// <TableTd>
// <Box w={100}>
// <a href={item.link} target="_blank" rel="noopener noreferrer">
// <Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
// </a>
// </Box>
// </TableTd>
// <TableTd>
// <Text>
// <Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
// <IconDeviceImac size={20} />
// </Button>
// </Text>
// </TableTd>
// </TableTr>
// ))}
// </TableTbody>
// </Table>
// </Paper>
// <Center>
// <Pagination
// value={page}
// onChange={(newPage) => {
// load(newPage, 10);
// window.scrollTo(0, 0);
// }}
// total={totalPages}
// mt="md"
// mb="md"
// />
// </Center>
// </Box>
// );
// }
// export default PelayananTelunjukSakti;
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
@@ -174,8 +18,7 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip,
Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -225,7 +68,6 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title>
<Tooltip label="Tambah Layanan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -236,7 +78,6 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover style={{ minWidth: '700px' }}>

View File

@@ -5,16 +5,17 @@ import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
ActionIcon,
Box,
Button,
Group,
Image,
Loader,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -30,6 +31,15 @@ function EditPenghargaan() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: "",
juara: "",
deskripsi: "",
imageId: "",
imageUrl: "",
});
// Lokal formData
const [formData, setFormData] = useState({
@@ -47,17 +57,25 @@ function EditPenghargaan() {
try {
const data = await statePenghargaan.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
juara: data.juara || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
const newForm = {
name: data.name || "",
juara: data.juara || "",
deskripsi: data.deskripsi || "",
imageId: data.imageId || "",
};
setFormData(newForm);
// simpan juga versi original
const imageUrl = data.image?.link || "";
setOriginalData({
...newForm,
imageUrl: imageUrl,
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
setPreviewImage(imageUrl || null);
}
} catch (error) {
console.error('Error loading penghargaan:', error);
@@ -68,33 +86,49 @@ function EditPenghargaan() {
loadPenghargaan();
}, [params?.id]);
const handleResetForm = () => {
setFormData({
name: originalData.name,
juara: originalData.juara,
deskripsi: originalData.deskripsi,
imageId: originalData.imageId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
// Submit
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// Sync ke global state saat submit
statePenghargaan.edit.form = {
...statePenghargaan.edit.form,
...formData,
};
// Upload file baru (kalau ada)
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');
return toast.error("Gagal upload gambar");
}
statePenghargaan.edit.form.imageId = uploaded.id;
imageId = uploaded.id;
}
// Update global state form (baru di sini)
statePenghargaan.edit.form = {
...statePenghargaan.edit.form,
name: formData.name,
juara: formData.juara,
deskripsi: formData.deskripsi,
imageId,
}
await statePenghargaan.edit.update();
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');
} finally {
setIsSubmitting(false);
}
};
@@ -102,11 +136,9 @@ function EditPenghargaan() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Tombol Back + Title */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Penghargaan
</Title>
@@ -155,7 +187,7 @@ function EditPenghargaan() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -174,25 +206,47 @@ function EditPenghargaan() {
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
<Box pos="relative" mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Box>
<Image
src={previewImage.startsWith('http') ? previewImage : `${window.location.origin}${previewImage}`}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
loading="lazy"
/>
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
@@ -212,6 +266,17 @@ function EditPenghargaan() {
{/* Tombol Simpan */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -222,7 +287,7 @@ function EditPenghargaan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -1,9 +1,5 @@
'use client'
import React, { useState } from 'react';
import penghargaanState from '../../../_state/desa/penghargaan';
import { useProxy } from 'valtio/utils';
import { useParams, useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import colors from '@/con/colors';
import {
Box,
Button,
@@ -12,12 +8,15 @@ import {
Paper,
Skeleton,
Stack,
Text,
Tooltip,
Text
} from '@mantine/core';
import colors from '@/con/colors';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import penghargaanState from '../../../_state/desa/penghargaan';
function DetailPenghargaan() {
const statePenghargaan = useProxy(penghargaanState);
@@ -127,34 +126,30 @@ function DetailPenghargaan() {
</Box>
<Group gap="sm" mt={10}>
<Tooltip label="Hapus Penghargaan" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
<Tooltip label="Edit Penghargaan" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(`/admin/desa/penghargaan/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
<Button
color="green"
onClick={() =>
router.push(`/admin/desa/penghargaan/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -2,16 +2,17 @@
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
ActionIcon,
Box,
Button,
Group,
Image,
Loader,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -27,6 +28,7 @@ function CreatePenghargaan() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
statePenghargaan.create.form = {
@@ -40,37 +42,43 @@ function CreatePenghargaan() {
};
const handleSubmit = async () => {
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
try {
setIsSubmitting(true);
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
statePenghargaan.create.form.imageId = uploaded.id;
await statePenghargaan.create.create();
resetForm();
router.push('/admin/desa/penghargaan');
} catch (error) {
console.error('Error creating penghargaan:', error);
toast.error('Terjadi kesalahan saat menambahkan penghargaan');
} finally {
setIsSubmitting(false);
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
statePenghargaan.create.form.imageId = uploaded.id;
await statePenghargaan.create.create();
resetForm();
router.push('/admin/desa/penghargaan');
};
return (
<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>
</Tooltip>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Penghargaan
</Title>
@@ -87,7 +95,7 @@ function CreatePenghargaan() {
>
<Stack gap="md">
<TextInput
defaultValue={statePenghargaan.create.form.name}
value={statePenghargaan.create.form.name}
onChange={(val) => (statePenghargaan.create.form.name = val.target.value)}
label="Nama Penghargaan"
placeholder="Masukkan nama penghargaan"
@@ -95,7 +103,7 @@ function CreatePenghargaan() {
/>
<TextInput
defaultValue={statePenghargaan.create.form.juara}
value={statePenghargaan.create.form.juara}
onChange={(val) => (statePenghargaan.create.form.juara = val.target.value)}
label="Juara"
placeholder="Masukkan juara"
@@ -125,7 +133,7 @@ function CreatePenghargaan() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -146,7 +154,7 @@ function CreatePenghargaan() {
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
@@ -154,12 +162,41 @@ function CreatePenghargaan() {
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
{/* Button Submit */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -170,7 +207,7 @@ function CreatePenghargaan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -18,8 +18,7 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip
Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -69,7 +68,6 @@ function ListPenghargaan({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Penghargaan</Title>
<Tooltip label="Tambah Penghargaan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -78,7 +76,6 @@ function ListPenghargaan({ search }: { search: string }) {
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconCategory, IconListDetails } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconListDetails, IconCategory } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -14,15 +14,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
label: "List Pengumuman",
value: "listpengumuman",
href: "/admin/desa/pengumuman/list-pengumuman",
icon: <IconListDetails size={18} stroke={1.8} />,
tooltip: "Lihat semua daftar pengumuman"
icon: <IconListDetails size={18} stroke={1.8} />
},
{
label: "Kategori Pengumuman",
value: "kategoripengumuman",
href: "/admin/desa/pengumuman/kategori-pengumuman",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori pengumuman"
icon: <IconCategory size={18} stroke={1.8} />
},
];
@@ -70,19 +68,18 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>

View File

@@ -11,7 +11,7 @@ import {
Stack,
TextInput,
Title,
Tooltip,
Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -23,8 +23,9 @@ function EditKategoriPengumuman() {
const editState = useProxy(stateDesaPengumuman.category);
const router = useRouter();
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ name: '' });
const [originalData, setOriginalData] = useState({ name: '' });
// Load data awal sekali aja
useEffect(() => {
@@ -36,6 +37,7 @@ function EditKategoriPengumuman() {
const data = await editState.update.load(id);
if (data) {
setFormData({ name: data.name || '' });
setOriginalData({ name: data.name || '' });
}
} catch (error) {
console.error('Error loading kategori Pengumuman:', error);
@@ -55,6 +57,7 @@ function EditKategoriPengumuman() {
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// Update global state hanya di sini
editState.update.form = {
...editState.update.form,
@@ -67,14 +70,23 @@ function EditKategoriPengumuman() {
} catch (error) {
console.error('Error updating kategori Pengumuman:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
} finally {
setIsSubmitting(false);
}
};
const handleResetForm = () => {
setFormData({
name: originalData.name,
});
toast.info("Form dikembalikan ke data awal");
};
return (
<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()}
@@ -83,7 +95,6 @@ function EditKategoriPengumuman() {
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Pengumuman
</Title>
@@ -108,6 +119,17 @@ function EditKategoriPengumuman() {
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -118,7 +140,7 @@ function EditKategoriPengumuman() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -9,15 +9,18 @@ import {
Stack,
TextInput,
Title,
Tooltip
Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import { toast } from 'react-toastify';
function CreateKategoriPengumuman() {
const createState = useProxy(stateDesaPengumuman.category);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
createState.create.form = {
@@ -26,16 +29,22 @@ function CreateKategoriPengumuman() {
};
const handleSubmit = async () => {
await createState.create.create();
resetForm();
router.push('/admin/desa/pengumuman/kategori-pengumuman');
try {
await createState.create.create();
resetForm();
router.push('/admin/desa/pengumuman/kategori-pengumuman');
} catch (error) {
console.error(error);
toast.error('Gagal menambahkan kategori pengumuman');
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
@@ -44,7 +53,6 @@ function CreateKategoriPengumuman() {
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Pengumuman
</Title>
@@ -63,12 +71,23 @@ function CreateKategoriPengumuman() {
<TextInput
label="Nama Kategori Pengumuman"
placeholder="Masukkan nama kategori pengumuman"
defaultValue={createState.create.form.name || ''}
value={createState.create.form.name || ''}
onChange={(e) => (createState.create.form.name = e.target.value)}
required
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -79,7 +98,7 @@ function CreateKategoriPengumuman() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -2,11 +2,13 @@
'use client'
import colors from '@/con/colors';
import {
Box, Button, Center, Paper, Skeleton, Stack,
Box, Button, Center,
Pagination,
Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
Text, Title, Tooltip, Pagination
Text, Title
} from '@mantine/core';
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -66,16 +68,14 @@ function ListKategoriPengumuman({ search }: { search: string }) {
<Stack>
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
<Title order={4}>List Kategori Pengumuman</Title>
<Tooltip label="Tambah Kategori Pengumuman" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
>
Tambah Baru
</Button>
</Tooltip>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
>
Tambah Baru
</Button>
</Box>
<Box style={{ overflowX: 'auto' }}>
@@ -99,29 +99,25 @@ function ListKategoriPengumuman({ search }: { search: string }) {
<Text truncate lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd>
<Tooltip label="Edit Kategori Pengumuman" withArrow>
<Button
variant='light'
color='green'
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
>
<IconEdit size={20} />
</Button>
</Tooltip>
<Button
variant='light'
color='green'
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Tooltip label="Hapus Kategori Pengumuman" withArrow>
<Button
variant='light'
color='red'
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</Tooltip>
<Button
variant='light'
color='red'
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))

View File

@@ -14,7 +14,7 @@ import {
Text,
TextInput,
Title,
Tooltip,
Loader
} from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
@@ -34,6 +34,15 @@ function EditPengumuman() {
content: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
judul: "",
deskripsi: "",
categoryPengumumanId: "",
content: "",
});
// Load kategori & pengumuman by id saat pertama kali
useEffect(() => {
editState.category.findMany.load();
@@ -51,6 +60,12 @@ function EditPengumuman() {
categoryPengumumanId: data.categoryPengumumanId || "",
content: data.content || "",
});
setOriginalData({
judul: data.judul || "",
deskripsi: data.deskripsi || "",
categoryPengumumanId: data.categoryPengumumanId || "",
content: data.content || "",
});
}
} catch (error) {
console.error("Error loading pengumuman:", error);
@@ -67,6 +82,7 @@ function EditPengumuman() {
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// update global state hanya sekali pas submit
editState.pengumuman.edit.form = {
...editState.pengumuman.edit.form,
@@ -79,22 +95,32 @@ function EditPengumuman() {
} catch (error) {
console.error("Error updating pengumuman:", error);
toast.error("Terjadi kesalahan saat memperbarui pengumuman");
} finally {
setIsSubmitting(false);
}
};
const handleResetForm = () => {
setFormData({
judul: originalData.judul,
deskripsi: originalData.deskripsi,
categoryPengumumanId: originalData.categoryPengumumanId,
content: originalData.content,
});
toast.info("Form dikembalikan ke data awal");
};
return (
<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>
</Tooltip>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Pengumuman
</Title>
@@ -155,17 +181,29 @@ function EditPengumuman() {
</Box>
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
color: "#fff",
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -1,5 +1,7 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import {
Box,
@@ -8,16 +10,13 @@ import {
Paper,
Skeleton,
Stack,
Text,
Tooltip,
Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
export default function DetailPengumuman() {
const pengumumanState = useProxy(stateDesaPengumuman);
@@ -117,7 +116,6 @@ export default function DetailPengumuman() {
</Box>
<Group gap="sm">
<Tooltip label="Hapus Pengumuman" withArrow position="top">
<Button
color="red"
onClick={() => {
@@ -130,9 +128,7 @@ export default function DetailPengumuman() {
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Pengumuman" withArrow position="top">
<Button
color="green"
onClick={() =>
@@ -146,7 +142,6 @@ export default function DetailPengumuman() {
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>

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