From a6663bbcee4180b8d97f6228d81770dd16fdf528 Mon Sep 17 00:00:00 2001 From: nico Date: Tue, 28 Oct 2025 17:34:38 +0800 Subject: [PATCH 01/97] QC Kak Inno 27 Oct QC Kak Ayu 27 Oct QC Keano 27 Oct QC Pak Jun 27 Oct --- .../darmasaba/(pages)/desa/potensi/page.tsx | 4 +- .../desa/profile/ui/profilPerbekel.tsx | 8 +- .../ekonomi/lowongan-kerja-lokal/page.tsx | 2 +- .../inovasi/ajukan-ide-inovatif/page.tsx | 2 + .../info-teknologi-tepat-guna/page.tsx | 21 +- .../administrasi-online/page.tsx | 8 +- .../detail-berita/[id]/page.tsx | 85 ++++++ .../detail-pengumuman/[id]/page.tsx | 62 ++++ .../informasi-desa/page.tsx | 287 ++++++++++++------ .../pengaduan-masyarakat/page.tsx | 14 +- .../artikel-kesehatan-page/page.tsx | 6 +- .../fasilitas-kesehatan-page/page.tsx | 8 +- .../jadwal-kegiatan-page/page.tsx | 6 +- .../info-wabah-penyakit/[id]/page.tsx | 1 - .../(pages)/kesehatan/kontak-darurat/page.tsx | 4 +- .../kesehatan/penanganan-darurat/page.tsx | 51 ++-- .../kesehatan/program-kesehatan/[id]/page.tsx | 8 +- .../kesehatan/program-kesehatan/page.tsx | 15 +- .../(pages)/kesehatan/puskesmas/page.tsx | 19 +- .../component/edukasiCard.tsx | 5 +- .../(pages)/pendidikan/beasiswa-desa/page.tsx | 55 ++-- .../pelajari-lebih-lanjut/page.tsx | 47 +-- .../bimbingan-belajar-desa/page.tsx | 16 +- .../[jenjangPendidikan]/content.tsx | 165 +++++----- .../info-sekolah/_lib/layoutTabs.tsx | 2 + .../pendidikan/info-sekolah/semua/page.tsx | 19 +- .../page.tsx | 46 +-- 27 files changed, 629 insertions(+), 337 deletions(-) create mode 100644 src/app/darmasaba/(pages)/inovasi/layanan-online-desa/informasi-desa/detail-berita/[id]/page.tsx create mode 100644 src/app/darmasaba/(pages)/inovasi/layanan-online-desa/informasi-desa/detail-pengumuman/[id]/page.tsx diff --git a/src/app/darmasaba/(pages)/desa/potensi/page.tsx b/src/app/darmasaba/(pages)/desa/potensi/page.tsx index 5f2e1236..2bcced1d 100644 --- a/src/app/darmasaba/(pages)/desa/potensi/page.tsx +++ b/src/app/darmasaba/(pages)/desa/potensi/page.tsx @@ -2,7 +2,7 @@ 'use client' import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import colors from '@/con/colors'; -import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; import { IconEye } from '@tabler/icons-react'; import { useTransitionRouter } from 'next-view-transitions'; import { useEffect, useState } from 'react'; @@ -114,7 +114,6 @@ function Page() { - - diff --git a/src/app/darmasaba/(pages)/desa/profile/ui/profilPerbekel.tsx b/src/app/darmasaba/(pages)/desa/profile/ui/profilPerbekel.tsx index 887ffdaf..461f4b2a 100644 --- a/src/app/darmasaba/(pages)/desa/profile/ui/profilPerbekel.tsx +++ b/src/app/darmasaba/(pages)/desa/profile/ui/profilPerbekel.tsx @@ -95,7 +95,7 @@ function ProfilPerbekel() { - + Biodata - + Pengalaman - + Pengalaman Organisasi - + Program Kerja Unggulan diff --git a/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/page.tsx b/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/page.tsx index 00ee1683..7507e6ce 100644 --- a/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/page.tsx @@ -52,7 +52,7 @@ function Page() { - + Lowongan Kerja Lokal diff --git a/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx b/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx index 5aafcf44..25408048 100644 --- a/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx @@ -55,6 +55,7 @@ function Page() { }} > + Tujuan Ide Inovatif Ini Mendorong partisipasi aktif masyarakat @@ -62,6 +63,7 @@ function Page() { Memecahkan tantangan komunal Mengembangkan potensi kreativitas warga + diff --git a/src/app/darmasaba/(pages)/inovasi/info-teknologi-tepat-guna/page.tsx b/src/app/darmasaba/(pages)/inovasi/info-teknologi-tepat-guna/page.tsx index 6a36f0f2..ca16da90 100644 --- a/src/app/darmasaba/(pages)/inovasi/info-teknologi-tepat-guna/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/info-teknologi-tepat-guna/page.tsx @@ -71,11 +71,22 @@ function Page() { {filteredData.map((v, k) => { return ( - - {v.name} - - - + + + {v.name} + + + + ) })} diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx index ca88dddc..4f848982 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx @@ -75,18 +75,18 @@ function AdministrasiOnline() { Ajukan Administrasi Online Nama} - placeholder="masukkan nama" + placeholder="Masukkan nama" onChange={(val) => (state.administrasiOnline.create.form.name = val.target.value)} /> Alamat} - placeholder="masukkan alamat" + placeholder="Masukkan alamat" onChange={(val) => (state.administrasiOnline.create.form.alamat = val.target.value)} /> Nomor Telepon} - placeholder="masukkan nomor telepon" + placeholder="Masukkan nomor telepon" onChange={(val) => (state.administrasiOnline.create.form.nomorTelepon = val.target.value)} /> - Program Beasiswa Pendidikan Desa Darmasaba + Program Beasiswa Pendidikan Desa Darmasaba Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba @@ -84,7 +84,7 @@ export default function BeasiswaPage() { - Tentang Program + Tentang Program Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses @@ -94,12 +94,11 @@ export default function BeasiswaPage() { {/* Tambahkan info tahun berjalan di sini */} - - 📅 Periode Beasiswa Tahun 2025 + + Periode Beasiswa Tahun 2025 - Pendaftaran beasiswa dibuka mulai 1 Januari 2025 dan ditutup pada - 31 Mei 2025. + Pendaftaran beasiswa dibuka mulai 1 Januari 2025 dan ditutup pada 31 Mei 2025. Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba. @@ -109,7 +108,7 @@ export default function BeasiswaPage() { - Syarat Pendaftaran + Syarat Pendaftaran @@ -140,7 +139,7 @@ export default function BeasiswaPage() { - Proses Seleksi + Proses Seleksi @@ -148,8 +147,8 @@ export default function BeasiswaPage() { Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung. - - ⏰ Estimasi waktu: 1 Februari – 31 Mei 2025 + + Estimasi waktu: 1 Februari – 31 Mei 2025 @@ -157,8 +156,8 @@ export default function BeasiswaPage() { Panitia memverifikasi kelengkapan dan validitas berkas. - - ⏰ Estimasi waktu: 5–7 hari kerja setelah penutupan pendaftaran + + Estimasi waktu: 5–7 hari kerja setelah penutupan pendaftaran @@ -166,8 +165,8 @@ export default function BeasiswaPage() { Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi. - - ⏰ Estimasi waktu: 7–10 hari kerja setelah pengumuman seleksi administrasi + + Estimasi waktu: 7–10 hari kerja setelah pengumuman seleksi administrasi @@ -175,14 +174,14 @@ export default function BeasiswaPage() { Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba. - - ⏰ Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai + + Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai - 🗓️ Total estimasi keseluruhan proses: sekitar 3–4 minggu setelah penutupan pendaftaran + Total estimasi keseluruhan proses: sekitar 3–4 minggu setelah penutupan pendaftaran @@ -191,7 +190,7 @@ export default function BeasiswaPage() { - Cerita Sukses Penerima Beasiswa + Cerita Sukses Penerima Beasiswa @@ -219,12 +218,12 @@ export default function BeasiswaPage() { - Siap Bergabung dengan Program Ini? + Siap Bergabung dengan Program Ini? Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba. - @@ -269,7 +268,11 @@ export default function BeasiswaPage() { { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} /> + value={beasiswaDesa.create.form.kewarganegaraan || "WNI"} // tampilkan WNI kalau kosong + onChange={(e) => { + beasiswaDesa.create.form.kewarganegaraan = e.target.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 ( + + {Icon && } + {option.label} + + ); + }} leftSection={ - IconComponent && ( - - + value && iconMap[value as IconKey] ? ( + + {(() => { + const Icon = iconMap[value as IconKey].icon; + return ; + })()} - ) + ) : 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} /> ); -} - +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/_state/desa/penghargaan.ts b/src/app/admin/(dashboard)/_state/desa/penghargaan.ts index 68be0ba7..20921427 100644 --- a/src/app/admin/(dashboard)/_state/desa/penghargaan.ts +++ b/src/app/admin/(dashboard)/_state/desa/penghargaan.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts index 09320003..dd289918 100644 --- a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts +++ b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts b/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts index fd3dd897..6b09c7e5 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts @@ -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) { + 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 }, diff --git a/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts b/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts index bf29247b..3f4da69b 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts @@ -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; diff --git a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts index 2eb11a03..5acad685 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts @@ -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, diff --git a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts index c45ac24a..4fe9939a 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts @@ -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; diff --git a/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts b/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts index 227c6b8c..1cf5941b 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts @@ -47,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"); diff --git a/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts b/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts index 1332bbea..8de0655e 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts b/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts index 341398f0..0c78ec15 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts @@ -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: "", diff --git a/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts b/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts index 52cd2f92..7625d59f 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts @@ -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: "", diff --git a/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts b/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts index 83d4d5cd..7afacdc2 100644 --- a/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts +++ b/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts b/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts index 710bbcd8..7facc93b 100644 --- a/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts +++ b/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts b/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts index 5e1d10ab..125d6b71 100644 --- a/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts +++ b/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts @@ -41,7 +41,7 @@ const programKreatifState = proxy({ if (res.status === 200) { programKreatifState.findMany.load(); - toast.success("success create"); + toast.success("Sukses menambahkan"); return true; } diff --git a/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts b/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts index 3fd458f4..1a19352e 100644 --- a/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts +++ b/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts b/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts index af0d2401..c3b70625 100644 --- a/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts +++ b/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts b/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts index 6db65c2b..5e61039b 100644 --- a/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts +++ b/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts @@ -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); diff --git a/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts b/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts index 8530d809..644bf0dd 100644 --- a/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts +++ b/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts b/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts index ad1ee158..4bd4609c 100644 --- a/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts +++ b/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts index 71c04389..016f6b28 100644 --- a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts +++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts @@ -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; diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts index 19f7e80a..15b7b4b8 100644 --- a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts +++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts @@ -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; diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts index 4da79df8..e00ab588 100644 --- a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts +++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts @@ -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; diff --git a/src/app/admin/(dashboard)/_state/landing-page/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts index be6b2e7f..e3a5fe18 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/profile.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts b/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts index f7092245..b1bf9143 100644 --- a/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts +++ b/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts b/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts index 584bad91..80750349 100644 --- a/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts +++ b/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts b/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts index 84d3083e..982bdadb 100644 --- a/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts +++ b/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts @@ -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"); diff --git a/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts b/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts index 08189a83..f8ffa416 100644 --- a/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts +++ b/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts @@ -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: "", diff --git a/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts b/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts index 61f0f23b..07e3b5be 100644 --- a/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts +++ b/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts @@ -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) { diff --git a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts index 4eb184ad..ad74dc6b 100644 --- a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts +++ b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts @@ -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: "", diff --git a/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts b/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts index 56b734d9..0a452cdd 100644 --- a/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts +++ b/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts @@ -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) { diff --git a/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts b/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts index 92373c3e..fc316fa9 100644 --- a/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts +++ b/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts @@ -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) { diff --git a/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts b/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts index 0a6304b8..4d0a3df4 100644 --- a/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts +++ b/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts @@ -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; \ No newline at end of file +export default stateProfilePPID; diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx index 14494532..2e86057b 100644 --- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx @@ -10,7 +10,8 @@ import { Paper, Stack, TextInput, - Title + Title, + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,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: '', @@ -38,6 +44,9 @@ function EditKategoriBerita() { setFormData({ name: data.name || '', }); + setOriginalData({ + name: data.name || '', + }); } } catch (error) { console.error('Error loading kategori Berita:', error); @@ -55,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, @@ -69,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); } }; @@ -109,6 +128,17 @@ function EditKategoriBerita() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx index 197ae1b7..be739576 100644 --- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx @@ -8,15 +8,19 @@ import { Paper, Stack, TextInput, - Title + Title, + 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 = { @@ -25,9 +29,17 @@ 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 ( @@ -60,12 +72,23 @@ function CreateKategoriBerita() { (createState.create.form.name = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx index 7c181f33..48cd08ea 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx @@ -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, @@ -15,7 +16,8 @@ import { Stack, Text, TextInput, - Title + Title, + Loader } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; import { @@ -44,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(); @@ -63,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); } @@ -82,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, @@ -108,21 +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 ( {/* Header */} - + Edit Berita @@ -216,14 +254,14 @@ function EditBerita() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -254,17 +310,29 @@ function EditBerita() { {/* Action */} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx index 8789e590..9a1d5acb 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx @@ -13,7 +13,9 @@ import { Stack, Text, TextInput, - Title + Title, + Loader, + ActionIcon } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useShallowEffect } from '@mantine/hooks'; @@ -28,6 +30,7 @@ export default function CreateBerita() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); useShallowEffect(() => { beritaState.kategoriBerita.findMany.load(); @@ -46,40 +49,48 @@ export default function CreateBerita() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + beritaState.berita.create.form.imageId = uploaded.id; + + await beritaState.berita.create.create(); + + resetForm(); + router.push('/admin/desa/berita/list-berita'); + } catch (error) { + console.error('Error creating berita:', error); + toast.error('Terjadi kesalahan saat membuat berita'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - beritaState.berita.create.form.imageId = uploaded.id; - - await beritaState.berita.create.create(); - - resetForm(); - router.push('/admin/desa/berita/list-berita'); }; return ( {/* Header dengan tombol kembali */} - + Tambah Berita @@ -97,7 +108,7 @@ export default function CreateBerita() { (beritaState.berita.create.form.judul = e.target.value)} required /> @@ -109,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( @@ -154,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" > @@ -175,7 +186,7 @@ export default function CreateBerita() { {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -204,6 +235,17 @@ export default function CreateBerita() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx index 30933f6f..21387eb2 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx @@ -4,15 +4,17 @@ 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, Paper, Stack, TextInput, - Title + Title, + Loader } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; +import { IconArrowBack, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; @@ -24,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: '', @@ -44,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); @@ -61,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); } }; @@ -88,14 +120,14 @@ function EditVideo() { return ( - + Edit Video @@ -127,7 +159,7 @@ function EditVideo() { required /> {embedLink && ( - + + /> + { + setFormData({ + ...formData, + linkVideo: '', + }); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -151,6 +203,17 @@ function EditVideo() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx index 6c4f5cc7..3e53d4cd 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx @@ -3,6 +3,7 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import colors from '@/con/colors'; import { + ActionIcon, Box, Button, Group, @@ -10,9 +11,10 @@ import { Stack, Text, TextInput, - Title + Title, + 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'; @@ -24,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 = { @@ -35,29 +38,37 @@ function CreateVideo() { }; const handleSubmit = async () => { - if (!embedLink) { - toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); - return; - } + try { + setIsSubmitting(true); + if (!embedLink) { + toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); + return; + } - videoState.create.form.linkVideo = embedLink; - await videoState.create.create(); - resetForm(); - router.push('/admin/desa/gallery/video'); + videoState.create.form.linkVideo = embedLink; + await videoState.create.create(); + resetForm(); + router.push('/admin/desa/gallery/video'); + } catch (error) { + console.error("Error creating video:", error); + toast.error("Terjadi kesalahan saat menambahkan video"); + } finally { + setIsSubmitting(false); + } }; return ( {/* Header Back Button + Title */} - + Tambah Video @@ -77,7 +88,7 @@ function CreateVideo() { { videoState.create.form.name = e.currentTarget.value; }} @@ -88,14 +99,14 @@ function CreateVideo() { setLink(e.currentTarget.value)} required /> {/* Preview Video */} {embedLink && ( - + + /> + { + setLink(''); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -125,6 +153,17 @@ function CreateVideo() { {/* Button Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx index ac92c919..0ee0d2e2 100644 --- a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, @@ -23,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: '', @@ -50,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); @@ -68,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, @@ -79,6 +109,8 @@ function EditAjukanPermohonan() { } catch (error) { console.error('Error updating ajukan:', error); toast.error('Terjadi kesalahan saat memperbarui ajukan'); + } finally { + setIsSubmitting(false); } }; @@ -86,9 +118,9 @@ function EditAjukanPermohonan() { {/* Back Button */} - + Edit Ajukan Permohonan @@ -153,6 +185,17 @@ function EditAjukanPermohonan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx index 41b4027a..a425a358 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -32,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 () => { @@ -45,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); @@ -57,39 +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 ( - + Edit Pelayanan Penduduk Non Permanent @@ -127,25 +156,31 @@ function EditPelayananPendudukNonPermanent() { {/* Submit Button */} - - - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx index a59a202a..9fdc6c56 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Skeleton, Stack, @@ -34,13 +35,21 @@ function EditPelayananPerizinanBerusaha() { 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); @@ -52,6 +61,12 @@ function EditPelayananPerizinanBerusaha() { 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"); } @@ -62,10 +77,10 @@ function EditPelayananPerizinanBerusaha() { setLoading(false); } }; - + loadData(); }, [id]); - + const handleChange = (field: keyof typeof formData) => @@ -76,13 +91,26 @@ function EditPelayananPerizinanBerusaha() { })); }; + 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); } }; @@ -99,14 +127,14 @@ function EditPelayananPerizinanBerusaha() { {/* Header */} - + Edit Pelayanan Perizinan Berusaha @@ -147,23 +175,31 @@ function EditPelayananPerizinanBerusaha() { /> - - - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx index b245d8de..f91b84b8 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx @@ -1,124 +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 + Title, } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { 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>; + preview: string | null; + setPreview: React.Dispatch>; +} + +// 🔹 File Uploader Component +const FileUploader: React.FC = ({ + title, + file, + setFile, + preview, + setPreview +}) => { + const handleDrop = (files: File[]) => { + const selected = files[0]; + if (selected) { + setFile(selected); + setPreview(URL.createObjectURL(selected)); + } + }; + + const handleRemove = () => { + setPreview(null); + setFile(null); + }; + + return ( + + + {title} + + toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format .png, .jpg, .jpeg, .webp + + + + + + {preview && ( + + Preview Gambar + + + + + )} + + ); +}; + +// 🔹 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({ name: '', deskripsi: '', imageId: '', image2Id: '', + imageUrl: '', + image2Url: '', }); - - // state file upload + const [originalData, setOriginalData] = useState(formData); const [file, setFile] = useState(null); const [file2, setFile2] = useState(null); - - // state preview gambar const [previewImage, setPreviewImage] = useState(null); const [previewImage2, setPreviewImage2] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); - // load data awal + // 🧭 Load Initial Data useEffect(() => { const loadSurat = async () => { const id = params?.id as string; if (!id) return; try { - const data = await stateSurat.edit.load(id); + const data = await stateLayananDesa.suratKeterangan.edit.load(id); if (!data) return; - setFormData((prev) => ({ - ...prev, - ...{ - name: prev.name || data.name || "", - deskripsi: prev.deskripsi || data.deskripsi || "", - imageId: prev.imageId || data.imageId || "", - image2Id: prev.image2Id || data.image2Id || "", - }, - })); + const mapped: FormData = { + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + image2Id: data.image2Id || '', + imageUrl: data.image?.link || '', + image2Url: data.image2?.link || '' + }; - if (data.image?.link && !previewImage) setPreviewImage(data.image.link); - if (data.image2?.link && !previewImage2) setPreviewImage2(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"); + console.error('Error loading surat:', error); + toast.error('Gagal memuat data surat'); } }; loadSurat(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [params?.id]); + // 📤 Upload File Helper + const uploadFile = async (file: File): Promise => { + 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 uploading file:', error); + return null; + } + }; + // 🔁 Reset Form + const handleResetForm = () => { + setFormData(originalData); + setPreviewImage(originalData.imageUrl || null); + setPreviewImage2(originalData.image2Url || null); + setFile(null); + setFile2(null); + toast.info('Form dikembalikan ke data awal'); + }; - // 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) => { + setFormData(prev => ({ ...prev, name: e.target.value })); + }; + + const handleDeskripsiChange = (html: string) => { + setFormData(prev => ({ ...prev, deskripsi: html })); + }; return ( - {/* Back Button */} + {/* Header */} - + Edit Surat Keterangan + {/* Form */} - {/* Input nama */} + {/* Nama Surat */} setFormData((prev) => ({ ...prev, name: e.target.value }))} + onChange={handleNameChange} required /> - {/* Input deskripsi */} + {/* Deskripsi */} Konten - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) - } + onChange={handleDeskripsiChange} /> - {/* Upload Gambar 1 */} - - - Gambar Konten Pelayanan - - { - const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error('File tidak valid, gunakan format gambar')} - maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} - radius="md" - p="xl" - > - - - - - - - - - - - - - Seret gambar atau klik untuk memilih file - - - Maksimal 5MB, format gambar wajib - - - - + {/* Gambar 1 */} + - {previewImage && ( - - Preview Gambar 1 - - )} - - - {/* Upload Gambar 2 */} - - - Gambar Alur Pelayanan Surat - - { - const selectedFile = files[0]; - if (selectedFile) { - setFile2(selectedFile); - setPreviewImage2(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error('File tidak valid, gunakan format gambar')} - maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} - radius="md" - p="xl" - > - - - - - - - - - - - - - Seret gambar atau klik untuk memilih file - - - Maksimal 5MB, format gambar wajib - - - - - - {previewImage2 && ( - - Preview Gambar 2 - - )} - + {/* Gambar 2 */} + + {/* Action Buttons */} + @@ -284,4 +352,4 @@ function EditSuratKeterangan() { ); } -export default EditSuratKeterangan; +export default EditSuratKeterangan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx index 0ccbbb23..9c065b52 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx @@ -5,10 +5,12 @@ 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, @@ -27,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 = { @@ -45,6 +48,7 @@ function CreateSuratKeterangan() { } try { + setIsSubmitting(true); // Upload gambar utama const res1 = await ApiFetch.api.fileStorage.create.post({ file: previewImage.file, @@ -77,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); } }; @@ -84,9 +90,9 @@ function CreateSuratKeterangan() { {/* Header */} - + Tambah Surat Keterangan @@ -103,7 +109,7 @@ function CreateSuratKeterangan() { {/* Nama Surat */} (stateSurat.create.form.name = val.target.value)} label="Nama Surat Keterangan" placeholder="Masukkan nama surat keterangan" @@ -140,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" > @@ -161,7 +167,7 @@ function CreateSuratKeterangan() { {previewImage && ( - + Preview Gambar Utama + { + setPreviewImage(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -190,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" > @@ -211,7 +234,7 @@ function CreateSuratKeterangan() { {previewImage2 ? ( - + Preview Gambar Tambahan + { + setPreviewImage2(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : ( @@ -229,6 +269,17 @@ function CreateSuratKeterangan() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx index 67bd5688..08ae9730 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -21,6 +22,7 @@ function EditPelayananTelunjukSakti() { const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -28,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 () => { @@ -42,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); @@ -60,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, @@ -73,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); } }; @@ -125,6 +150,17 @@ function EditPelayananTelunjukSakti() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx index 3ebc515f..28faa0ff 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -13,12 +14,14 @@ import { } 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 = { @@ -30,6 +33,7 @@ function CreatePelayananTelunjukDesa() { const handleSubmit = async () => { try { + setIsSubmitting(true); await stateTelunjukDesa.create.create(); resetForm(); toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan'); @@ -37,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); } }; @@ -64,7 +70,7 @@ function CreatePelayananTelunjukDesa() { {/* Nama */} { stateTelunjukDesa.create.form.name = val.target.value; }} @@ -75,7 +81,7 @@ function CreatePelayananTelunjukDesa() { {/* Deskripsi */} { stateTelunjukDesa.create.form.deskripsi = val.target.value; }} @@ -86,7 +92,7 @@ function CreatePelayananTelunjukDesa() { {/* Link */} { stateTelunjukDesa.create.form.link = val.target.value; }} @@ -97,6 +103,17 @@ function CreatePelayananTelunjukDesa() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx index bbf9b7d6..53ec0c12 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx @@ -5,10 +5,12 @@ 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, @@ -29,6 +31,15 @@ function EditPenghargaan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + juara: "", + deskripsi: "", + imageId: "", + imageUrl: "", + }); // Lokal formData const [formData, setFormData] = useState({ @@ -46,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); @@ -67,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); } }; @@ -152,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" > @@ -171,25 +206,47 @@ function EditPenghargaan() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - - Preview Gambar + + Preview Gambar + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); }} - loading="lazy" - /> + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -209,6 +266,17 @@ function EditPenghargaan() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx index e3089f6d..a76063c9 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -26,6 +28,7 @@ function CreatePenghargaan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { statePenghargaan.create.form = { @@ -39,26 +42,34 @@ 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 ( @@ -84,7 +95,7 @@ function CreatePenghargaan() { > (statePenghargaan.create.form.name = val.target.value)} label="Nama Penghargaan" placeholder="Masukkan nama penghargaan" @@ -92,7 +103,7 @@ function CreatePenghargaan() { /> (statePenghargaan.create.form.juara = val.target.value)} label="Juara" placeholder="Masukkan juara" @@ -122,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" > @@ -143,7 +154,7 @@ function CreatePenghargaan() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Button Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx index 1ed44942..1438347d 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx @@ -10,7 +10,8 @@ import { Paper, Stack, TextInput, - Title + Title, + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,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(() => { @@ -35,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); @@ -54,6 +57,7 @@ function EditKategoriPengumuman() { const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state hanya di sini editState.update.form = { ...editState.update.form, @@ -66,9 +70,19 @@ function EditKategoriPengumuman() { } catch (error) { console.error('Error updating kategori Pengumuman:', error); toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + return ( {/* Header */} @@ -105,6 +119,17 @@ function EditKategoriPengumuman() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx index e7515eb0..da1fb9c4 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx @@ -8,15 +8,19 @@ import { Paper, Stack, TextInput, - Title + Title, + 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 = { @@ -25,9 +29,16 @@ 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 ( @@ -60,12 +71,23 @@ function CreateKategoriPengumuman() { (createState.create.form.name = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx index 086f119a..cbeeceef 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx @@ -13,7 +13,8 @@ import { Stack, Text, TextInput, - Title + Title, + Loader } from "@mantine/core"; import { IconArrowBack } from "@tabler/icons-react"; import { useParams, useRouter } from "next/navigation"; @@ -33,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(); @@ -50,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); @@ -66,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, @@ -78,9 +95,21 @@ 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 ( @@ -152,17 +181,29 @@ function EditPengumuman() { + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx index fe2bea16..44a40fb0 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx @@ -12,25 +12,37 @@ import { Stack, Text, TextInput, - Title + Title, + Loader } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreatePengumuman() { const pengumumanState = useProxy(stateDesaPengumuman); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); useShallowEffect(() => { pengumumanState.category.findMany.load(); }, []); const handleSubmit = async () => { - await pengumumanState.pengumuman.create.create(); - resetForm(); - router.push('/admin/desa/pengumuman/list-pengumuman'); + try { + setIsSubmitting(true); + await pengumumanState.pengumuman.create.create(); + resetForm(); + router.push('/admin/desa/pengumuman/list-pengumuman'); + } catch (error) { + console.error('Error creating pengumuman:', error); + toast.error('Terjadi kesalahan saat membuat pengumuman'); + } finally { + setIsSubmitting(false); + } }; const resetForm = () => { @@ -46,9 +58,9 @@ function CreatePengumuman() { {/* Header */} - + Tambah Pengumuman @@ -65,7 +77,7 @@ function CreatePengumuman() { {/* Judul */} (pengumumanState.pengumuman.create.form.judul = val.target.value)} label="Judul" placeholder="Masukkan judul pengumuman" @@ -76,21 +88,32 @@ function CreatePengumuman() { ({ + label: item.nama, + value: item.id, + })) || []} + value={formData.kategoriId || null} + onChange={(val: string | null) => { + if (val) { + const selected = potensiDesaState.kategoriPotensi.findMany.data?.find( + (item) => item.id === val + ); + if (selected) { + handleChange("kategoriId", selected.id); + } + } else { + handleChange("kategoriId", ""); + } + }} + searchable + clearable + nothingFoundMessage="Tidak ditemukan" + required + /> + + {/* ({ + label: item.nama, + value: item.id, + })) || []} + value={potensiState.create.form.kategoriId || null} + onChange={(val: string | null) => { + if (val) { + const selected = potensiDesaState.kategoriPotensi.findMany.data?.find( + (item) => item.id === val + ); + if (selected) { + potensiState.create.form.kategoriId = selected.id; + } + } else { + potensiState.create.form.kategoriId = ''; + } + }} + searchable + clearable + nothingFoundMessage="Tidak ditemukan" + required + /> + + {/* Layanan Polsek} + label="Layanan Polsek" placeholder="Pilih layanan polsek" data={layananOptions} - value={polsekState.create.form.layananPolsekId} - onChange={(val) => (polsekState.create.form.layananPolsekId = val || "")} + value={polsekState.create.form.layananPolsekId || null} + onChange={(val: string | null) => { + if (val) { + const selected = layananOptions.find( + (item) => item.value === val + ); + if (selected) { + polsekState.create.form.layananPolsekId = selected.value; + } + } else { + polsekState.create.form.layananPolsekId = ''; + } + }} + searchable + clearable + nothingFoundMessage="Tidak ditemukan" + required /> + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx index cc44dc55..e6bb7f88 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx @@ -1,10 +1,12 @@ "use client"; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -36,12 +38,20 @@ function EditTipsKeamanan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ judul: "", deskripsi: "", imageId: "", }); + const [originalData, setOriginalData] = useState({ + judul: "", + deskripsi: "", + imageId: "", + imageUrl: "", + }); + // Load data saat pertama kali useEffect(() => { const loadData = async () => { @@ -57,6 +67,13 @@ function EditTipsKeamanan() { imageId: data.imageId || "", }); + setOriginalData({ + judul: data.judul || "", + deskripsi: data.deskripsi || "", + imageId: data.imageId || "", + imageUrl: data.image?.link || "", + }) + if (data?.image?.link) { setPreviewImage(data.image.link); } @@ -73,6 +90,7 @@ function EditTipsKeamanan() { const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; if (file) { @@ -97,9 +115,22 @@ function EditTipsKeamanan() { } catch (error) { console.error("Error updating tips keamanan:", error); toast.error("Terjadi kesalahan saat memperbarui tips keamanan"); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + return ( {/* Header */} @@ -167,14 +198,14 @@ function EditTipsKeamanan() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage ? ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : ( + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx index 13ecc9bb..898d9905 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -26,6 +28,7 @@ function CreateKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateKeamanan.create.form = { @@ -38,27 +41,35 @@ function CreateKeamananLingkungan() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + stateKeamanan.create.form.imageId = uploaded.id; + + await stateKeamanan.create.create(); + + resetForm(); + router.push('/admin/keamanan/tips-keamanan'); + } catch (error) { + console.error("Error creating tips keamanan:", error); + toast.error("Gagal menambahkan tips keamanan"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - stateKeamanan.create.form.imageId = uploaded.id; - - await stateKeamanan.create.create(); - - resetForm(); - router.push('/admin/keamanan/tips-keamanan'); }; return ( @@ -98,7 +109,7 @@ function CreateKeamananLingkungan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -119,7 +130,7 @@ function CreateKeamananLingkungan() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -135,7 +164,7 @@ function CreateKeamananLingkungan() { (stateKeamanan.create.form.judul = e.target.value)} required /> @@ -155,6 +184,17 @@ function CreateKeamananLingkungan() { {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx index 62a2b28a..3ffdd614 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx @@ -4,10 +4,12 @@ import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -26,6 +28,7 @@ function CreateArtikelKesehatan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateArtikelKesehatan.create.form = { @@ -62,25 +65,32 @@ function CreateArtikelKesehatan() { const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + stateArtikelKesehatan.create.form.imageId = uploaded.id; + await stateArtikelKesehatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan'); + } catch (error) { + console.error('Error submitting form:', error); + toast.error('Gagal menyimpan data, silakan coba lagi'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - stateArtikelKesehatan.create.form.imageId = uploaded.id; - await stateArtikelKesehatan.create.submit(); - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan'); }; return ( @@ -124,7 +134,7 @@ function CreateArtikelKesehatan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -145,7 +155,7 @@ function CreateArtikelKesehatan() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -164,7 +192,7 @@ function CreateArtikelKesehatan() { { stateArtikelKesehatan.create.form.title = e.target.value; }} @@ -173,7 +201,7 @@ function CreateArtikelKesehatan() { { stateArtikelKesehatan.create.form.content = e.target.value; }} @@ -196,7 +224,7 @@ function CreateArtikelKesehatan() { label={"Judul Gejala"} required placeholder="Masukkan judul gejala penyakit" - defaultValue={stateArtikelKesehatan.create.form.symptom.title} + value={stateArtikelKesehatan.create.form.symptom.title} onChange={(e) => { stateArtikelKesehatan.create.form.symptom.title = e.target.value; }} @@ -220,7 +248,7 @@ function CreateArtikelKesehatan() { label={"Judul Pencegahan"} required placeholder="Masukkan judul" - defaultValue={stateArtikelKesehatan.create.form.prevention.title} + value={stateArtikelKesehatan.create.form.prevention.title} onChange={(e) => { stateArtikelKesehatan.create.form.prevention.title = e.target.value; }} @@ -241,7 +269,7 @@ function CreateArtikelKesehatan() { label={"Judul Pertolongan Pertama"} required placeholder="Masukkan judul" - defaultValue={stateArtikelKesehatan.create.form.firstAid.title} + value={stateArtikelKesehatan.create.form.firstAid.title} onChange={(e) => { stateArtikelKesehatan.create.form.firstAid.title = e.target.value; }} @@ -262,7 +290,7 @@ function CreateArtikelKesehatan() { label={"Judul Mitos dan Fakta"} required placeholder="Masukkan judul" - defaultValue={stateArtikelKesehatan.create.form.mythVsFact.title} + value={stateArtikelKesehatan.create.form.mythVsFact.title} onChange={(e) => { stateArtikelKesehatan.create.form.mythVsFact.title = e.target.value; }} @@ -300,8 +328,20 @@ function CreateArtikelKesehatan() { {/* Submit Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx index 230e105d..567b7e68 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; @@ -8,6 +9,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -45,6 +47,7 @@ function EditFasilitasKesehatan() { const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -56,6 +59,16 @@ function EditFasilitasKesehatan() { tarifDanLayanan: { layanan: '', tarif: '' }, }); + const [originalData, setOriginalData] = useState({ + name: '', + informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' }, + layananUnggulan: { content: '' }, + dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' }, + fasilitasPendukung: { content: '' }, + prosedurPendaftaran: { content: '' }, + tarifDanLayanan: { layanan: '', tarif: '' }, + }); + // Helper untuk update nested state const updateForm = ( key: K, @@ -71,26 +84,73 @@ function EditFasilitasKesehatan() { [key]: { ...prev[key] as object, [nestedKey]: value }, })); + const deepClone = (obj: any): any => { + try { + return JSON.parse(JSON.stringify(obj)); + } catch (error) { + console.warn('Gagal deep clone dengan JSON fallback:', error); + return obj; // fallback (berisiko shared reference) + } + }; + // Load data useEffect(() => { const load = async () => { const id = params?.id as string; if (!id) return; + try { await state.edit.load(id); - const form = state.edit.form; - if (form) setFormData(form as FasilitasKesehatanFormBase); + const loadedData = state.edit.form; + + if (!loadedData) { + toast.error('Data tidak ditemukan'); + return; + } + + // Gunakan JSON fallback untuk deep clone + const clonedData = deepClone(loadedData) as FasilitasKesehatanFormBase; + + setFormData(clonedData); + setOriginalData(clonedData); } catch (err) { console.error(err); toast.error('Gagal memuat data fasilitas kesehatan'); } }; + load(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + informasiUmum: + { + fasilitas: originalData.informasiUmum.fasilitas, + alamat: originalData.informasiUmum.alamat, + jamOperasional: originalData.informasiUmum.jamOperasional + }, + layananUnggulan: { content: originalData.layananUnggulan.content }, + dokterdanTenagaMedis: { + name: originalData.dokterdanTenagaMedis.name, + specialist: originalData.dokterdanTenagaMedis.specialist, + jadwal: originalData.dokterdanTenagaMedis.jadwal + }, + fasilitasPendukung: { content: originalData.fasilitasPendukung.content }, + prosedurPendaftaran: { content: originalData.prosedurPendaftaran.content }, + tarifDanLayanan: { + layanan: originalData.tarifDanLayanan.layanan, + tarif: originalData.tarifDanLayanan.tarif + }, + }); + toast.info("Form dikembalikan ke data awal"); + }; + // Submit const handleSubmit = async () => { try { + setIsSubmitting(true); state.edit.form = { ...state.edit.form, ...formData }; const success = await state.edit.submit(); if (success) { @@ -100,6 +160,8 @@ function EditFasilitasKesehatan() { } catch (err) { console.error(err); toast.error('Terjadi kesalahan saat memperbarui data fasilitas kesehatan'); + } finally { + setIsSubmitting(false); } }; @@ -230,19 +292,30 @@ function EditFasilitasKesehatan() { {/* Tombol Simpan */} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx index 0a8ceeb9..4b61acfb 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -14,6 +15,7 @@ import { } 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'; @@ -21,6 +23,7 @@ import { useProxy } from 'valtio/utils'; function CreateFasilitasKesehatan() { const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateFasilitasKesehatan.create.form = { @@ -53,10 +56,18 @@ function CreateFasilitasKesehatan() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await stateFasilitasKesehatan.create.submit(); - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'); + try { + setIsSubmitting(true); + await stateFasilitasKesehatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'); + } catch (error) { + console.error(error); + toast.error('Gagal menyimpan data'); + } finally { + setIsSubmitting(false); + } }; return ( @@ -89,7 +100,7 @@ function CreateFasilitasKesehatan() { (stateFasilitasKesehatan.create.form.name = e.target.value)} required /> @@ -100,21 +111,21 @@ function CreateFasilitasKesehatan() { (stateFasilitasKesehatan.create.form.informasiUmum.fasilitas = e.target.value)} required /> (stateFasilitasKesehatan.create.form.informasiUmum.alamat = e.target.value)} required /> (stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional = e.target.value)} required /> @@ -135,21 +146,21 @@ function CreateFasilitasKesehatan() { (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value)} required /> (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value)} required /> (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value)} required /> @@ -179,23 +190,35 @@ function CreateFasilitasKesehatan() { (stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value)} required /> (stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value)} required /> {/* Submit */} - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx index 71bf934b..3e33d305 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx @@ -40,7 +40,7 @@ function CreateDokter() { Nama Dokter} placeholder="masukkan nama dokter" - defaultValue={createState.create.create.form.name} + value={createState.create.create.form.name} onChange={(e) => { createState.create.create.form.name = e.target.value; }} @@ -49,7 +49,7 @@ function CreateDokter() { Specialist} placeholder="masukkan specialist" - defaultValue={createState.create.create.form.specialist} + value={createState.create.create.form.specialist} onChange={(e) => { createState.create.create.form.specialist = e.target.value; }} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx index a4acdfbf..be757981 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx @@ -19,7 +19,7 @@ import { Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -28,18 +28,10 @@ import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_wa function FasilitasKesehatan() { - const router = useRouter(); const [search, setSearch] = useState(""); return ( - {/* Tombol Back */} - - - - {/* Header Search */} { const loadData = async () => { @@ -39,13 +50,25 @@ function EditGrafikHasilKepuasan() { try { const data = await editState.update.load(id); - if (data) setFormData({ - nama: data.nama || '', - tanggal: data.tanggal || '', - jenisKelamin: data.jenisKelamin || '', - alamat: data.alamat || '', - penyakit: data.penyakit || '', - }); + if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + + setFormData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyakit: data.penyakit || '', + }); + + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyakit: data.penyakit || '', + }); + } } catch (err) { console.error("Error loading grafik hasil kepuasan:", err); toast.error("Gagal memuat data grafik hasil kepuasan"); @@ -60,8 +83,20 @@ function EditGrafikHasilKepuasan() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + penyakit: originalData.penyakit, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); editState.update.form = { ...editState.update.form, ...formData }; await editState.update.submit(); toast.success('Grafik hasil kepuasan berhasil diperbarui!'); @@ -69,6 +104,8 @@ function EditGrafikHasilKepuasan() { } catch (err) { console.error('Error updating grafik hasil kepuasan:', err); toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan'); + } finally { + setIsSubmitting(false); } }; @@ -112,6 +149,17 @@ function EditGrafikHasilKepuasan() { ))} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx index 4a42acbb..d1ea8b7f 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -15,12 +16,14 @@ import { import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateGrafikHasilKepuasanMasyarakat() { const stateGrafikKepuasan = useProxy(grafikkepuasan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateGrafikKepuasan.create.form = { @@ -33,9 +36,17 @@ function CreateGrafikHasilKepuasanMasyarakat() { }; const handleSubmit = async () => { - await stateGrafikKepuasan.create.create(); - resetForm(); - router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); + try { + setIsSubmitting(true); + await stateGrafikKepuasan.create.create(); + resetForm(); + router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); + } catch (error) { + console.error("Error creating grafik kepuasan:", error); + toast.error("Terjadi kesalahan saat membuat grafik kepuasan"); + } finally { + setIsSubmitting(false); + } }; return ( @@ -68,7 +79,7 @@ function CreateGrafikHasilKepuasanMasyarakat() { (stateGrafikKepuasan.create.form.nama = e.target.value)} required /> @@ -76,33 +87,44 @@ function CreateGrafikHasilKepuasanMasyarakat() { type="date" label="Tanggal" placeholder="Masukkan tanggal" - defaultValue={stateGrafikKepuasan.create.form.tanggal} + value={stateGrafikKepuasan.create.form.tanggal} onChange={(e) => (stateGrafikKepuasan.create.form.tanggal = e.target.value)} required /> (stateGrafikKepuasan.create.form.jenisKelamin = e.target.value)} required /> (stateGrafikKepuasan.create.form.alamat = e.target.value)} required /> (stateGrafikKepuasan.create.form.penyakit = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx index 88ce54d1..9252f2e4 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx @@ -4,7 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -39,7 +39,10 @@ function EditJadwalKegiatan() { const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState(emptyForm()); + const [originalData, setOriginalData] = useState(emptyForm()); + // Helper untuk update nested state const updateNested = < @@ -85,6 +88,19 @@ function EditJadwalKegiatan() { syaratKetentuanJadwalKegiatan: { content: form.syaratKetentuanJadwalKegiatan?.content || '' }, dokumenJadwalKegiatan: { content: form.dokumenJadwalKegiatan?.content || '' }, }); + setOriginalData({ + content: form.content || '', + informasiJadwalKegiatan: { + name: form.informasiJadwalKegiatan?.name || '', + tanggal: form.informasiJadwalKegiatan?.tanggal || '', + waktu: form.informasiJadwalKegiatan?.waktu || '', + lokasi: form.informasiJadwalKegiatan?.lokasi || '', + }, + deskripsiJadwalKegiatan: { deskripsi: form.deskripsiJadwalKegiatan?.deskripsi || '' }, + layananJadwalKegiatan: { content: form.layananJadwalKegiatan?.content || '' }, + syaratKetentuanJadwalKegiatan: { content: form.syaratKetentuanJadwalKegiatan?.content || '' }, + dokumenJadwalKegiatan: { content: form.dokumenJadwalKegiatan?.content || '' }, + }); } } catch (error) { console.error("Error loading jadwal kegiatan:", error); @@ -94,8 +110,26 @@ function EditJadwalKegiatan() { loadJadwalKegiatan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + content: originalData.content || '', + informasiJadwalKegiatan: { + name: originalData.informasiJadwalKegiatan?.name || '', + tanggal: originalData.informasiJadwalKegiatan?.tanggal || '', + waktu: originalData.informasiJadwalKegiatan?.waktu || '', + lokasi: originalData.informasiJadwalKegiatan?.lokasi || '', + }, + deskripsiJadwalKegiatan: { deskripsi: originalData.deskripsiJadwalKegiatan?.deskripsi || '' }, + layananJadwalKegiatan: { content: originalData.layananJadwalKegiatan?.content || '' }, + syaratKetentuanJadwalKegiatan: { content: originalData.syaratKetentuanJadwalKegiatan?.content || '' }, + dokumenJadwalKegiatan: { content: originalData.dokumenJadwalKegiatan?.content || '' }, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateJadwalKegiatan.edit.form = { ...stateJadwalKegiatan.edit.form, ...formData }; const success = await stateJadwalKegiatan.edit.submit(); if (success) { @@ -105,6 +139,8 @@ function EditJadwalKegiatan() { } catch (error) { console.error("Error updating jadwal kegiatan:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data jadwal kegiatan"); + } finally { + setIsSubmitting(false); } }; @@ -190,6 +226,17 @@ function EditJadwalKegiatan() { {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx index a63a5c8e..4cf617fb 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -14,12 +15,14 @@ import { } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJadwalKegiatan() { const stateJadwalKegiatan = useProxy(jadwalKegiatanState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { stateJadwalKegiatan.create.form = { @@ -47,11 +50,18 @@ function CreateJadwalKegiatan() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await stateJadwalKegiatan.create.submit(); - - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan'); + try { + setIsSubmitting(true); + await stateJadwalKegiatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan'); + } catch (error) { + console.error(error); + toast.error('Gagal menyimpan data jadwal kegiatan'); + } finally { + setIsSubmitting(false); + } }; return ( @@ -84,7 +94,7 @@ function CreateJadwalKegiatan() { { stateJadwalKegiatan.create.form.content = e.target.value; }} @@ -107,7 +117,7 @@ function CreateJadwalKegiatan() { label="Nama" required placeholder="Masukkan nama" - defaultValue={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name} + value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name} onChange={(e) => { stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name = e.target.value; }} @@ -116,7 +126,7 @@ function CreateJadwalKegiatan() { type="date" required label="Tanggal" - defaultValue={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal} + value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal} onChange={(e) => { stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal = e.target.value; }} @@ -125,7 +135,7 @@ function CreateJadwalKegiatan() { label="Waktu" required placeholder="Masukkan waktu" - defaultValue={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu} + value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu} onChange={(e) => { stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu = e.target.value; }} @@ -134,7 +144,7 @@ function CreateJadwalKegiatan() { label="Lokasi" required placeholder="Masukkan lokasi" - defaultValue={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi} + value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi} onChange={(e) => { stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi = e.target.value; }} @@ -172,8 +182,20 @@ function CreateJadwalKegiatan() { {/* Save Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx index 91370aef..c8d517a2 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx @@ -19,7 +19,7 @@ import { Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -27,18 +27,10 @@ import HeaderSearch from '../../../_com/header'; import jadwalKegiatanState from '../../../_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; function JadwalKegiatan() { - const router = useRouter(); const [search, setSearch] = useState(""); return ( - {/* Tombol Back */} - - - - {/* Header Search */} { const loadKelahiran = async () => { const id = params?.id as string; if (!id) return; - + try { const data = await editState.edit.load(id); - if (data) setFormData({ - nama: data.nama || '', - tanggal: data.tanggal || '', - jenisKelamin: data.jenisKelamin || '', - alamat: data.alamat || '' - }); + if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + + setFormData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '' + }); + + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '' + }); + } } catch (error) { console.error('Error loading data kelahiran:', error); toast.error('Gagal memuat data kelahiran'); } }; - + loadKelahiran(); }, [params?.id]); @@ -57,8 +78,20 @@ function EditKelahiran() { setFormData((prev) => ({ ...prev, [key]: value })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state hanya saat submit editState.edit.form = { ...editState.edit.form, ...formData }; await editState.edit.update(); @@ -67,6 +100,8 @@ function EditKelahiran() { } catch (error) { console.error('Error updating data kelahiran:', error); toast.error('Terjadi kesalahan saat memperbarui data kelahiran'); + } finally { + setIsSubmitting(false); } }; @@ -123,6 +158,17 @@ function EditKelahiran() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx index 71d7721f..90ea1275 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx @@ -5,6 +5,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -13,13 +14,15 @@ import { } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKelahiran() { const createState = useProxy(persentaseKelahiranKematian.kelahiran); const router = useRouter(); - + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { createState.create.form = { @@ -32,11 +35,19 @@ function CreateKelahiran() { const handleSubmit = async () => { - await createState.create.create(); - resetForm(); - router.push( - '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran' - ); + try { + setIsSubmitting(true); + await createState.create.create(); + resetForm(); + router.push( + '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran' + ); + } catch (error) { + console.error('Error creating kelahiran:', error); + toast.error('Gagal menambahkan data kelahiran'); + } finally { + setIsSubmitting(false); + } }; @@ -71,7 +82,7 @@ function CreateKelahiran() { Nama} placeholder="Masukkan nama" - defaultValue={createState.create.form.nama} + value={createState.create.form.nama} onChange={(e) => (createState.create.form.nama = e.target.value)} required /> @@ -79,27 +90,38 @@ function CreateKelahiran() { type="date" label={Tanggal} placeholder="Masukkan tanggal" - defaultValue={createState.create.form.tanggal} + value={createState.create.form.tanggal} onChange={(e) => (createState.create.form.tanggal = e.target.value)} required /> Jenis Kelamin} placeholder="Masukkan jenis kelamin" - defaultValue={createState.create.form.jenisKelamin} + value={createState.create.form.jenisKelamin} onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)} required /> Alamat} placeholder="Masukkan alamat" - defaultValue={createState.create.form.alamat} + value={createState.create.form.alamat} onChange={(e) => (createState.create.form.alamat = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx index a70c1a42..2606a523 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx @@ -8,6 +8,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -19,11 +20,13 @@ import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +import { convertToISODate } from '../../../lib/dateUtils'; function EditKematian() { const editState = useProxy(persentaseKelahiranKematian.kematian); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', @@ -33,6 +36,14 @@ function EditKematian() { penyebab: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyebab: '', + }); + // Load data saat mount useEffect(() => { const loadData = async () => { @@ -42,12 +53,22 @@ function EditKematian() { try { const data = await editState.edit.load(id); if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + setFormData({ nama: data.nama || '', - tanggal: data.tanggal || '', + tanggal: formattedTanggal, jenisKelamin: data.jenisKelamin || '', alamat: data.alamat || '', - penyebab: data.penyebab || '', + penyebab: data.penyebab || '' + }); + + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyebab: data.penyebab || '' }); } } catch (error) { @@ -63,8 +84,20 @@ function EditKematian() { setFormData(prev => ({ ...prev, [key]: value })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + penyebab: originalData.penyebab, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state saat submit editState.edit.form = { ...editState.edit.form, ...formData }; await editState.edit.update(); @@ -75,6 +108,8 @@ function EditKematian() { } catch (error) { console.error('Error updating data kematian:', error); toast.error('Terjadi kesalahan saat memperbarui data kematian'); + } finally { + setIsSubmitting(false); } }; @@ -144,6 +179,17 @@ function EditKematian() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx index 8d1e0790..7248597a 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx @@ -6,6 +6,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, @@ -13,18 +14,14 @@ import { } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - - - - - function CreateKematian() { const createState = useProxy(persentaseKelahiranKematian.kematian); const router = useRouter(); - + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { createState.create.form = { @@ -38,19 +35,27 @@ function CreateKematian() { const handleSubmit = async () => { - if (!createState.create.form.nama) { - return toast.warn('Nama wajib diisi'); - } - if (!createState.create.form.tanggal) { - return toast.warn('Tanggal wajib diisi'); - } + try { + setIsSubmitting(true); + if (!createState.create.form.nama) { + return toast.warn('Nama wajib diisi'); + } + if (!createState.create.form.tanggal) { + return toast.warn('Tanggal wajib diisi'); + } - await createState.create.create(); - resetForm(); - router.push( - '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian' - ); + await createState.create.create(); + resetForm(); + router.push( + '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian' + ); + } catch (error) { + console.error('Error creating data kematian:', error); + toast.error('Gagal menambahkan data kematian'); + } finally { + setIsSubmitting(false); + } }; @@ -80,7 +85,7 @@ function CreateKematian() { (createState.create.form.nama = e.target.value)} required /> @@ -88,21 +93,21 @@ function CreateKematian() { type="date" label="Tanggal" placeholder="Masukkan tanggal" - defaultValue={createState.create.form.tanggal} + value={createState.create.form.tanggal} onChange={(e) => (createState.create.form.tanggal = e.target.value)} required /> (createState.create.form.jenisKelamin = e.target.value)} required /> (createState.create.form.alamat = e.target.value)} required /> @@ -120,6 +125,17 @@ function CreateKematian() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx new file mode 100644 index 00000000..3ce629bc --- /dev/null +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx @@ -0,0 +1,24 @@ +export const convertToISODate = (dateString: string): string => { + if (!dateString) return ''; + + // Jika format dd/mm/yyyy + const parts = dateString.split('/'); + if (parts.length === 3 && parts[0].length === 2 && parts[1].length === 2 && parts[2].length === 4) { + const [day, month, year] = parts; + return `${year}-${month}-${day}`; + } + + // Jika sudah format YYYY-MM-DD, biarkan + if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { + return dateString; + } + + // Jika format lain, coba parse dengan Date + const date = new Date(dateString); + if (!isNaN(date.getTime())) { + return date.toISOString().split('T')[0]; // YYYY-MM-DD + } + + console.warn(`Format tanggal tidak dikenali: ${dateString}`); + return ''; +}; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx index a00cf493..39473173 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx @@ -5,10 +5,12 @@ import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wab import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -26,13 +28,22 @@ function EditInfoWabahPenyakit() { const infoWabahPenyakitState = useProxy(infoWabahPenyakit); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsiSingkat: '', - deskripsi: '', + deskripsiLengkap: '', imageId: '', }); + + const [originalData, setOriginalData] = useState({ + name: '', + deskripsiSingkat: '', + deskripsiLengkap: '', + imageId: '', + imageUrl: '' + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); @@ -53,9 +64,16 @@ function EditInfoWabahPenyakit() { setFormData({ name: data.name || '', deskripsiSingkat: data.deskripsiSingkat || '', - deskripsi: data.deskripsiLengkap || '', + deskripsiLengkap: data.deskripsiLengkap || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsiLengkap: data.deskripsiLengkap || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data.image?.link) setPreviewImage(data.image.link); } @@ -70,6 +88,7 @@ function EditInfoWabahPenyakit() { const handleSubmit = async () => { try { + setIsSubmitting(true); let uploadedImageId = formData.imageId; // Upload file kalau ada @@ -86,7 +105,7 @@ function EditInfoWabahPenyakit() { ...infoWabahPenyakitState.edit.form, name: formData.name, deskripsiSingkat: formData.deskripsiSingkat, - deskripsiLengkap: formData.deskripsi, + deskripsiLengkap: formData.deskripsiLengkap, imageId: uploadedImageId, }; @@ -96,9 +115,23 @@ function EditInfoWabahPenyakit() { } catch (error) { console.error(error); toast.error('Terjadi kesalahan saat memperbarui info wabah penyakit'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsiSingkat: originalData.deskripsiSingkat, + deskripsiLengkap: originalData.deskripsiLengkap, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleDrop = (files: File[]) => { const selectedFile = files[0]; if (selectedFile) { @@ -149,11 +182,11 @@ function EditInfoWabahPenyakit() { - Deskripsi + Deskripsi Lengkap updateField('deskripsi', val)} + value={formData.deskripsiLengkap} + onChange={(val) => updateField('deskripsiLengkap', val)} /> @@ -165,7 +198,7 @@ function EditInfoWabahPenyakit() { onDrop={handleDrop} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -190,7 +223,7 @@ function EditInfoWabahPenyakit() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx index f55b0e68..d51ddc31 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -26,6 +28,7 @@ function CreateInfoWabahPenyakit() { const infoWabahPenyakitState = useProxy(infoWabahPenyakit) const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { infoWabahPenyakitState.create.form = { @@ -39,25 +42,33 @@ function CreateInfoWabahPenyakit() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn("Pilih file gambar terlebih dahulu"); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error("Gagal upload gambar"); + } + + infoWabahPenyakitState.create.form.imageId = uploaded.id; + await infoWabahPenyakitState.create.create(); + + resetForm(); + router.push("/admin/kesehatan/info-wabah-penyakit") + } catch (error) { + console.error("Error creating info wabah penyakit:", error); + toast.error("Gagal menambahkan info wabah penyakit"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } - - infoWabahPenyakitState.create.form.imageId = uploaded.id; - await infoWabahPenyakitState.create.create(); - - resetForm(); - router.push("/admin/kesehatan/info-wabah-penyakit") }; return ( @@ -88,7 +99,7 @@ function CreateInfoWabahPenyakit() { > { infoWabahPenyakitState.create.form.name = val.target.value; }} @@ -129,7 +140,7 @@ function CreateInfoWabahPenyakit() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -154,7 +165,7 @@ function CreateInfoWabahPenyakit() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx index 98014e3a..21af40ca 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx @@ -1,13 +1,16 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -28,12 +31,20 @@ function EditKontakDarurat() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsi: '', imageId: '', whatsapp: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + whatsapp: '', + imageUrl: '', + }); const [loading, setLoading] = useState(true); // Load data sekali saat mount @@ -51,6 +62,13 @@ function EditKontakDarurat() { imageId: data.imageId || '', whatsapp: data.whatsapp || '', }); + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + whatsapp: data.whatsapp || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } } catch (error) { @@ -62,10 +80,11 @@ function EditKontakDarurat() { }; loadKontakDarurat(); - }, [params?.id, kontakDaruratState.edit]); + }, [params?.id]); const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; // Upload file baru jika ada @@ -89,9 +108,23 @@ function EditKontakDarurat() { } catch (error) { console.error("Error updating kontak darurat:", error); toast.error("Terjadi kesalahan saat memperbarui kontak darurat"); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + whatsapp: originalData.whatsapp, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + if (loading) return Loading...; return ( @@ -151,7 +184,7 @@ function EditKontakDarurat() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -172,7 +205,7 @@ function EditKontakDarurat() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx index 4fc6d685..d35f1fb4 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -31,6 +33,7 @@ function CreateKontakDarurat() { const kontakDaruratState = useProxy(kontakDarurat); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { kontakDaruratState.create.form = { @@ -44,26 +47,34 @@ function CreateKontakDarurat() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + kontakDaruratState.create.form.imageId = uploaded.id; + + await kontakDaruratState.create.create(); + + resetForm(); + router.push('/admin/kesehatan/kontak-darurat'); + } catch (error) { + console.error('Error creating kontak darurat:', error); + toast.error('Gagal menambahkan kontak darurat'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - kontakDaruratState.create.form.imageId = uploaded.id; - - await kontakDaruratState.create.create(); - - resetForm(); - router.push('/admin/kesehatan/kontak-darurat'); }; return ( @@ -94,7 +105,7 @@ function CreateKontakDarurat() { > { kontakDaruratState.create.form.name = val.target.value; }} @@ -105,7 +116,7 @@ function CreateKontakDarurat() { { kontakDaruratState.create.form.whatsapp = val.target.value; }} @@ -136,7 +147,7 @@ function CreateKontakDarurat() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx index 47a054ae..ecf4d166 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx @@ -6,10 +6,12 @@ import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penangan import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -27,6 +29,7 @@ function EditPenangananDarurat() { const penangananDaruratState = useProxy(penangananDarurat) const router = useRouter(); const params = useParams() + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -34,6 +37,13 @@ function EditPenangananDarurat() { imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + imageUrl: '', + }); + const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const [loading, setLoading] = useState(true); @@ -53,6 +63,13 @@ function EditPenangananDarurat() { imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); + if (data.image?.link) { setPreviewImage(data.image.link); } @@ -80,8 +97,20 @@ function EditPenangananDarurat() { setPreviewImage(URL.createObjectURL(selected)); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; if (file) { @@ -107,6 +136,8 @@ function EditPenangananDarurat() { } catch (err) { console.error("Error updating penanganan darurat:", err); toast.error("Gagal memperbarui data penanganan darurat"); + } finally { + setIsSubmitting(false); } }; @@ -156,7 +187,7 @@ function EditPenangananDarurat() { onDrop={handleDrop} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -181,7 +212,7 @@ function EditPenangananDarurat() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx index 00839be0..c75878fb 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -30,6 +32,7 @@ function CreatePenangananDarurat() { const router = useRouter(); const penangananDaruratState = useProxy(penangananDarurat); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [file, setFile] = useState(null); const resetForm = () => { @@ -43,26 +46,34 @@ function CreatePenangananDarurat() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + penangananDaruratState.create.form.imageId = uploaded.id; + + await penangananDaruratState.create.create(); + + resetForm(); + router.push('/admin/kesehatan/penanganan-darurat'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan penanganan darurat'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - penangananDaruratState.create.form.imageId = uploaded.id; - - await penangananDaruratState.create.create(); - - resetForm(); - router.push('/admin/kesehatan/penanganan-darurat'); }; return ( @@ -96,7 +107,7 @@ function CreatePenangananDarurat() { Judul} placeholder="Masukkan judul" - defaultValue={penangananDaruratState.create.form.name} + value={penangananDaruratState.create.form.name} onChange={(val) => { penangananDaruratState.create.form.name = val.target.value; }} @@ -128,7 +139,7 @@ function CreatePenangananDarurat() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -190,6 +219,17 @@ function CreatePenangananDarurat() { {/* Button Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx index 8c90a4b6..8882f40a 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx @@ -6,10 +6,12 @@ import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/pos import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -30,6 +32,7 @@ function EditPosyandu() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', nomor: '', @@ -37,6 +40,14 @@ function EditPosyandu() { imageId: '', jadwalPelayanan: '', }); + const [originalData, setOriginalData] = useState({ + name: "", + nomor: "", + deskripsi: "", + imageId: "", + jadwalPelayanan: "", + imageUrl: "" + }); // Load data posyandu useEffect(() => { @@ -54,6 +65,14 @@ function EditPosyandu() { imageId: data.imageId || '', jadwalPelayanan: data.jadwalPelayanan || '', }); + setOriginalData({ + name: data.name || '', + nomor: data.nomor || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + jadwalPelayanan: data.jadwalPelayanan || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } } catch (error) { @@ -66,7 +85,7 @@ function EditPosyandu() { const handleSubmit = async () => { try { - // Update global state hanya saat submit + setIsSubmitting(true); const updatedForm = { ...statePosyandu.edit.form, ...formData }; // Upload file jika ada @@ -86,9 +105,24 @@ function EditPosyandu() { } catch (error) { console.error('Error updating posyandu:', error); toast.error('Terjadi kesalahan saat memperbarui posyandu'); + } finally { + setIsSubmitting(false); } }; + const resetForm = () => { + setFormData({ + name: originalData.name, + nomor: originalData.nomor, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + jadwalPelayanan: originalData.jadwalPelayanan, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + return ( {/* Tombol Back */} @@ -126,7 +160,7 @@ function EditPosyandu() { }} 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" > @@ -145,25 +179,45 @@ function EditPosyandu() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -209,6 +263,17 @@ function EditPosyandu() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx index 66fdb61c..606b21d6 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -27,6 +29,7 @@ function CreatePosyandu() { const router = useRouter(); const [file, setFile] = useState(null); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { @@ -43,35 +46,31 @@ function CreatePosyandu() { 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'); + } + // Upload gambar dulu + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + statePosyandu.create.form.imageId = uploaded.id; + await statePosyandu.create.create(); + resetForm(); + router.push('/admin/kesehatan/posyandu'); + } catch (error) { + console.error('Error creating posyandu:', error); + toast.error('Gagal menambahkan posyandu'); + } finally { + setIsSubmitting(false); } - - - // Upload gambar dulu - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - - statePosyandu.create.form.imageId = uploaded.id; - - - await statePosyandu.create.create(); - - - resetForm(); - router.push('/admin/kesehatan/posyandu'); }; - - return ( {/* Header */} @@ -109,7 +108,7 @@ function CreatePosyandu() { }} 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" > @@ -131,7 +130,7 @@ function CreatePosyandu() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -152,14 +169,14 @@ function CreatePosyandu() { (statePosyandu.create.form.name = e.target.value)} required /> (statePosyandu.create.form.nomor = e.target.value)} required /> @@ -189,6 +206,17 @@ function CreatePosyandu() { {/* Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx index 249ade53..ec68731d 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx @@ -5,10 +5,12 @@ import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-k import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -29,12 +31,20 @@ function EditProgramKesehatan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsiSingkat: '', deskripsi: '', imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsiSingkat: '', + deskripsi: '', + imageId: '', + imageUrl: '' + }); // Load data awal useEffect(() => { @@ -52,6 +62,13 @@ function EditProgramKesehatan() { deskripsi: data.deskripsi || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } catch (err) { @@ -68,9 +85,22 @@ function EditProgramKesehatan() { setFormData((prev) => ({ ...prev, [key]: value })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsiSingkat: originalData.deskripsiSingkat, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + // Submit form const handleSubmit = async () => { try { + setIsSubmitting(true); const updatedForm = { ...programKesehatanState.edit.form, ...formData }; // Upload file kalau ada @@ -89,6 +119,8 @@ function EditProgramKesehatan() { } catch (err) { console.error(err); toast.error('Terjadi kesalahan saat memperbarui program kesehatan'); + } finally { + setIsSubmitting(false); } }; @@ -158,7 +190,7 @@ function EditProgramKesehatan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -183,7 +215,7 @@ function EditProgramKesehatan() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx index 75537a6e..020c8f50 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -26,6 +28,7 @@ function CreateProgramKesehatan() { const programKesehatanState = useProxy(programKesehatan); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { programKesehatanState.create.form = { @@ -45,25 +48,33 @@ function CreateProgramKesehatan() { if (!programKesehatanState.create.form.deskripsiSingkat) { return toast.warn("Deskripsi singkat wajib diisi"); } - if (!file) { - return toast.warn("Pilih file gambar terlebih dahulu"); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error("Gagal upload gambar"); + } + + programKesehatanState.create.form.imageId = uploaded.id; + await programKesehatanState.create.create(); + + resetForm(); + router.push("/admin/kesehatan/program-kesehatan"); + } catch (error) { + console.error("Error creating program kesehatan:", error); + toast.error("Gagal menambahkan program kesehatan"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } - - programKesehatanState.create.form.imageId = uploaded.id; - await programKesehatanState.create.create(); - - resetForm(); - router.push("/admin/kesehatan/program-kesehatan"); }; return ( @@ -89,7 +100,7 @@ function CreateProgramKesehatan() { > { programKesehatanState.create.form.name = val.target.value; }} @@ -136,7 +147,7 @@ function CreateProgramKesehatan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -161,7 +172,7 @@ function CreateProgramKesehatan() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx index 61f43035..4b3a4350 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' @@ -5,10 +6,12 @@ import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/p import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -52,6 +55,7 @@ function EditPuskesmas() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', alamat: '', @@ -59,6 +63,14 @@ function EditPuskesmas() { kontak: { kontakPuskesmas: '', email: '', facebook: '', kontakUGD: '' }, imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + alamat: '', + jam: { workDays: '', weekDays: '', holiday: '' }, + kontak: { kontakPuskesmas: '', email: '', facebook: '', kontakUGD: '' }, + imageId: '', + imageUrl: '' + }); useEffect(() => { const loadPuskesmas = async () => { @@ -85,6 +97,24 @@ function EditPuskesmas() { }, imageId: form.imageId, }); + setOriginalData({ + name: form.name, + alamat: form.alamat, + jam: { + workDays: form.jam.workDays, + weekDays: form.jam.weekDays, + holiday: form.jam.holiday, + }, + kontak: { + kontakPuskesmas: form.kontak.kontakPuskesmas, + email: form.kontak.email, + facebook: form.kontak.facebook, + kontakUGD: form.kontak.kontakUGD, + }, + imageId: form.imageId, + imageUrl: (form as any).image?.link + + }); const formWithImage = form as PuskesmasFormData; if (formWithImage.image?.link) { @@ -99,8 +129,31 @@ function EditPuskesmas() { loadPuskesmas(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + alamat: originalData.alamat, + jam: { + workDays: originalData.jam.workDays, + weekDays: originalData.jam.weekDays, + holiday: originalData.jam.holiday, + }, + kontak: { + kontakPuskesmas: originalData.kontak.kontakPuskesmas, + email: originalData.kontak.email, + facebook: originalData.kontak.facebook, + kontakUGD: originalData.kontak.kontakUGD, + }, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); statePuskesmas.edit.form = { ...statePuskesmas.edit.form, name: formData.name, @@ -130,6 +183,8 @@ function EditPuskesmas() { } catch (error) { console.error("Error updating puskesmas:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data puskesmas"); + } finally { + setIsSubmitting(false); } }; @@ -252,7 +307,7 @@ function EditPuskesmas() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -276,7 +331,7 @@ function EditPuskesmas() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx index 59787839..62cdc64a 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx @@ -2,10 +2,12 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -25,6 +27,7 @@ function CreatePuskesmas() { const router = useRouter(); const [file, setFile] = useState(null); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const resetForm = () => { statePuskesmas.create.form = { @@ -50,35 +53,43 @@ function CreatePuskesmas() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + statePuskesmas.create.form.imageId = uploaded.id; + await statePuskesmas.create.submit(); + + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/puskesmas'); + } catch (error) { + console.error('Error creating posyandu:', error); + toast.error('Terjadi kesalahan saat membuat posyandu'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - statePuskesmas.create.form.imageId = uploaded.id; - await statePuskesmas.create.submit(); - - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/puskesmas'); }; return ( {/* Header */} - + Tambah Data Puskesmas @@ -97,40 +108,40 @@ function CreatePuskesmas() { (statePuskesmas.create.form.name = e.target.value)} required /> (statePuskesmas.create.form.alamat = e.target.value)} required /> (statePuskesmas.create.form.jam.workDays = e.target.value)} /> (statePuskesmas.create.form.jam.weekDays = e.target.value)} /> (statePuskesmas.create.form.jam.holiday = e.target.value)} /> (statePuskesmas.create.form.kontak.kontakPuskesmas = e.target.value) } @@ -138,19 +149,19 @@ function CreatePuskesmas() { (statePuskesmas.create.form.kontak.email = e.target.value)} /> (statePuskesmas.create.form.kontak.facebook = e.target.value)} /> (statePuskesmas.create.form.kontak.kontakUGD = e.target.value)} /> @@ -168,7 +179,7 @@ function CreatePuskesmas() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -193,7 +204,7 @@ function CreatePuskesmas() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Action Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx similarity index 62% rename from src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx rename to src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx index ca6d3b99..c1adc0ac 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx @@ -5,10 +5,12 @@ import sdgsDesa from "@/app/admin/(dashboard)/_state/landing-page/sdgs-desa"; import colors from "@/con/colors"; import ApiFetch from "@/lib/api-fetch"; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, @@ -16,7 +18,7 @@ import { Title } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; -import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; @@ -32,6 +34,15 @@ export default function EditKolaborasiInovasi() { jumlah: "", imageId: "", }); + + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + jumlah: "", + imageId: "", + imageUrl: "", + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); @@ -44,14 +55,21 @@ export default function EditKolaborasiInovasi() { try { const data = await sdgsState.edit.load(id); if (data) { - setFormData({ + // isi form awal + const newForm = { name: data.name || "", jumlah: data.jumlah || "", imageId: data.imageId || "", + }; + setFormData(newForm); + + // simpan juga versi original + setOriginalData({ + ...newForm, + imageUrl: data.image?.link || "", }); - if (data.image?.link) { - setPreviewImage(data.image.link); - } + + setPreviewImage(data.image?.link || null); } } catch (error) { console.error("Error loading sdgs desa:", error); @@ -62,12 +80,24 @@ export default function EditKolaborasiInovasi() { loadKolaborasi(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + jumlah: originalData.jumlah, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleInputChange = (field: keyof typeof formData, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; // Upload file baru jika ada @@ -83,19 +113,21 @@ export default function EditKolaborasiInovasi() { await sdgsState.edit.update(); toast.success("sdgs desa berhasil diperbarui!"); - router.push("/admin/landing-page/sdgs-desa"); + router.push("/admin/landing-page/SDGs"); } catch (error) { console.error("Error updating sdgs desa:", error); toast.error("Terjadi kesalahan saat memperbarui sdgs desa"); + } finally { + setIsSubmitting(false); } }; return ( - + Edit Sdgs Desa @@ -112,7 +144,7 @@ export default function EditKolaborasiInovasi() { - Gambar Sdgs Desa + Gambar Program Inovasi { @@ -122,15 +154,15 @@ export default function EditKolaborasiInovasi() { setPreviewImage(URL.createObjectURL(selectedFile)); } }} - onReject={() => toast.error("File tidak valid, gunakan format gambar")} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ "image/*": [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > - + @@ -143,25 +175,49 @@ export default function EditKolaborasiInovasi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + {/* ✅ Preview gambar + tombol X */} {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} - + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx similarity index 97% rename from src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx rename to src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx index 7cd5265d..3a754953 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx @@ -27,7 +27,7 @@ function DetailSDGSDesa() { sdgsState.delete.byId(selectedId) setModalHapus(false) setSelectedId(null) - router.push("/admin/landing-page/sdgs-desa") + router.push("/admin/landing-page/SDGs") } } @@ -111,7 +111,7 @@ function DetailSDGSDesa() { + + Tambah Sdgs Desa + + + + + + + + Gambar Program Inovasi + + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + + + + + + {/* ✅ Preview gambar + tombol X */} + {previewImage && ( + + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + + { + stateSDGSDesa.create.form.name = e.currentTarget.value; + }} + required + /> + + + Jumlah + + } + placeholder="Masukkan jumlah" + value={stateSDGSDesa.create.form.jumlah} + onChange={(val) => { + stateSDGSDesa.create.form.jumlah = val.target.value; + }} + required + min={0} + radius="md" + /> + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default CreateSDGsDesa; diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/page.tsx similarity index 97% rename from src/app/admin/(dashboard)/landing-page/sdgs-desa/page.tsx rename to src/app/admin/(dashboard)/landing-page/SDGs/page.tsx index 644ba1cd..5d4eb010 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/page.tsx @@ -63,7 +63,7 @@ function ListSdgsDesa({ search }: { search: string }) { leftSection={} color={colors['blue-button']} variant="light" - onClick={() => router.push('/admin/landing-page/sdgs-desa/create')} + onClick={() => router.push('/admin/landing-page/SDGs/create')} > Tambah Baru @@ -97,7 +97,7 @@ function ListSdgsDesa({ search }: { search: string }) { Daftar Sdgs Desa @@ -131,7 +131,7 @@ function ListSdgsDesa({ search }: { search: string }) { variant="light" color="blue" leftSection={} - onClick={() => router.push(`/admin/landing-page/sdgs-desa/${item.id}`)} + onClick={() => router.push(`/admin/landing-page/SDGs/${item.id}`)} > Detail diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx index bdfc7be5..10a262ce 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx @@ -13,7 +13,9 @@ import { Stack, Text, TextInput, - Title + Title, + Loader, + ActionIcon } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -34,6 +36,17 @@ function EditAPBDes() { fileId: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + jumlah: "", + imageId: "", + fileId: "", + imageUrl: "", + docUrl: "", + }); + const [previewImage, setPreviewImage] = useState(null); const [previewDoc, setPreviewDoc] = useState(null); const [imageFile, setImageFile] = useState(null); @@ -48,12 +61,21 @@ function EditAPBDes() { try { const data = await apbdesState.edit.load(id); if (data) { - setFormData({ - name: data.name || '', - jumlah: data.jumlah || '', - imageId: data.imageId || '', - fileId: data.fileId || '' + const newForm = { + name: data.name || "", + jumlah: data.jumlah || "", + imageId: data.imageId || "", + fileId: data.fileId || "", + }; + setFormData(newForm); + + // simpan juga versi original + setOriginalData({ + ...newForm, + imageUrl: data.image?.link || "", + docUrl: data.file?.link || "", }); + setPreviewImage(data.image?.link || null); setPreviewDoc(data.file?.link || null); } @@ -82,6 +104,7 @@ function EditAPBDes() { const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state with local form data first apbdesState.edit.form = { ...apbdesState.edit.form, ...formData }; @@ -103,13 +126,29 @@ function EditAPBDes() { await apbdesState.edit.update(); toast.success('APBDes berhasil diperbarui!'); - router.push('/admin/landing-page/apbdes'); + router.push('/admin/landing-page/APBDes'); } catch (err) { console.error(err); toast.error('Terjadi kesalahan saat memperbarui APBDes'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + jumlah: originalData.jumlah, + imageId: originalData.imageId, + fileId: originalData.fileId, + }); + setPreviewImage(originalData.imageUrl || null); + setImageFile(null); + setPreviewDoc(originalData.docUrl || null); + setDocFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + return ( @@ -147,7 +186,7 @@ function EditAPBDes() { onDrop={handleDrop('image')} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -157,13 +196,43 @@ function EditAPBDes() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - - Preview Gambar + + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setImageFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -196,16 +265,47 @@ function EditAPBDes() { {previewDoc && ( - + Dokumen terpilih: {docFile?.name || 'Dokumen'} + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewDoc(null); + setDocFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx index 4cd49a60..3b27da39 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx @@ -28,7 +28,7 @@ function DetailAPBDes() { apbdesState.delete.byId(selectedId) setModalHapus(false) setSelectedId(null) - router.push("/admin/landing-page/apbdes") + router.push("/admin/landing-page/APBDes") } } @@ -133,7 +133,7 @@ function DetailAPBDes() { + Tambah APBDes @@ -102,7 +107,7 @@ function CreateAPBDes() { {/* Gambar APBDes */} - Gambar APBDes + Gambar Program Inovasi { @@ -114,40 +119,65 @@ function CreateAPBDes() { }} 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" > - + - + - + - - + + Seret gambar atau klik untuk memilih file - - Maksimal 5MB (format: JPEG, JPG, PNG, GIF, WEBP, SVG) + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp - + + {/* ✅ Preview gambar + tombol X */} {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setImageFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -197,7 +227,7 @@ function CreateAPBDes() { {previewDoc && ( - + Pratinjau Dokumen @@ -207,6 +237,25 @@ function CreateAPBDes() { height="500px" style={{ border: '1px solid #ddd', borderRadius: '8px' }} /> + + { + setPreviewDoc(null); + setDocFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -215,19 +264,31 @@ function CreateAPBDes() { (stateAPBDes.create.form.name = e.target.value)} required /> (stateAPBDes.create.form.jumlah = e.target.value)} required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/page.tsx index 6c72569e..a8d35fc4 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/page.tsx @@ -61,7 +61,7 @@ function ListAPBDes({ search }: { search: string }) { leftSection={} color="blue" variant="light" - onClick={() => router.push('/admin/landing-page/apbdes/create')} + onClick={() => router.push('/admin/landing-page/APBDes/create')} > Tambah Baru @@ -116,7 +116,7 @@ function ListAPBDes({ search }: { search: string }) { variant="light" color="blue" leftSection={} - onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)} + onClick={() => router.push(`/admin/landing-page/APBDes/${item.id}`)} fullWidth > Detail diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx index b73a741d..1361aecf 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx @@ -1,9 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; +import { Box, Button, Group, Paper, Stack, TextInput, Title, Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; @@ -14,71 +15,78 @@ export default function EditKategoriDesaAntiKorupsi() { const router = useRouter(); const params = useParams(); const id = params?.id as string; - const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi); - // state lokal untuk form + // 🧠 Ambil proxy asli (bisa ditulis) & snapshot (buat render) + const stateKategori = korupsiState.kategoriDesaAntiKorupsi; + const snapshotKategori = useProxy(stateKategori); + + // 🧾 state lokal form const [formData, setFormData] = useState({ name: '' }); - const [isLoading, setIsLoading] = useState(false); + const [originalData, setOriginalData] = useState({ name: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); - // load data kategori saat mount atau id berubah + // 📥 load data saat pertama kali dibuka useEffect(() => { if (!id) return; const loadKategori = async () => { - setIsLoading(true); try { const data = await stateKategori.edit.load(id); if (data) { stateKategori.edit.id = id; - setFormData({ name: data.name || '' }); + const newForm = { name: data.name || '' }; + setFormData(newForm); + setOriginalData(newForm); } } catch (err) { console.error(err); toast.error('Gagal memuat data kategori desa anti korupsi'); - } finally { - setIsLoading(false); } }; loadKategori(); }, [id]); - // handler controlled input - const handleChange = useCallback( - (field: keyof typeof formData, value: string) => { - setFormData(prev => ({ ...prev, [field]: value })); - }, - [] - ); + // ✍️ ubah value input form + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; - // submit form + // 🔁 reset ke data awal + const handleResetForm = () => { + setFormData({ ...originalData }); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 submit update const handleSubmit = useCallback(async () => { if (!formData.name.trim()) return toast.error('Nama kategori tidak boleh kosong'); + setIsSubmitting(true); try { - setIsLoading(true); - - // update global state hanya saat submit + // isi form global dari local state stateKategori.edit.form = { name: formData.name.trim() }; - if (!stateKategori.edit.id) stateKategori.edit.id = id; + stateKategori.edit.id = id; await stateKategori.edit.update(); + toast.success('Kategori berhasil diperbarui'); router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi'); } catch (err) { console.error(err); toast.error(err instanceof Error ? err.message : 'Gagal memperbarui kategori'); } finally { - setIsLoading(false); + setIsSubmitting(false); } - }, [formData.name, id, router, stateKategori.edit]); + }, [formData.name, id, router]); + // 🧩 UI return ( - + Edit Kategori Desa Anti Korupsi @@ -96,25 +104,36 @@ export default function EditKategoriDesaAntiKorupsi() { handleChange('name', e.currentTarget.value)} required - disabled={isLoading} /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx index bb816f12..117f5f19 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx @@ -1,16 +1,18 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; +import { Box, Button, Group, Paper, Stack, TextInput, Title, Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi'; +import { toast } from 'react-toastify'; export default function CreateKategoriDesaAntiKorupsi() { const router = useRouter(); const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { stateKategori.findMany.load(); @@ -23,21 +25,29 @@ export default function CreateKategoriDesaAntiKorupsi() { }; const handleSubmit = async () => { - if (!stateKategori.create.form.name) { - return alert('Nama kategori harus diisi'); + setIsSubmitting(true); + try { + if (!stateKategori.create.form.name) { + return alert('Nama kategori harus diisi'); + } + + await stateKategori.create.create(); + resetForm(); + router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"); + } catch (error) { + console.error("Error creating kategori desa anti korupsi:", error); + toast.error("Gagal menambahkan kategori desa anti korupsi"); + } finally { + setIsSubmitting(false); } - - await stateKategori.create.create(); - resetForm(); - router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"); }; return ( - + Tambah Kategori Desa Anti Korupsi @@ -55,12 +65,24 @@ export default function CreateKategoriDesaAntiKorupsi() { (stateKategori.create.form.name = e.target.value)} required /> - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx index dfb4b0e6..eaf0c853 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; -import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Loader, ActionIcon, Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; @@ -34,9 +34,18 @@ export default function EditDesaAntiKorupsi() { fileId: '', }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + kategoriId: "", + fileId: "", + fileUrl: "" + }); + const [previewFile, setPreviewFile] = useState(null); const [file, setFile] = useState(null); - const [isLoading, setIsLoading] = useState(false); // Load kategori useShallowEffect(() => { @@ -63,6 +72,14 @@ export default function EditDesaAntiKorupsi() { fileId: data.fileId, }); + setOriginalData({ + name: data.name, + deskripsi: data.deskripsi, + kategoriId: data.kategoriId, + fileId: data.fileId, + fileUrl: data.file?.link || "", + }); + if (data.file?.link) setPreviewFile(data.file.link); } catch (err) { console.error(err); @@ -91,12 +108,24 @@ export default function EditDesaAntiKorupsi() { setPreviewFile(URL.createObjectURL(selectedFile)); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + kategoriId: originalData.kategoriId, + fileId: originalData.fileId, + }); + setPreviewFile(originalData.fileUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.name) return toast.warn('Masukkan judul dokumen'); if (!formData.kategoriId) return toast.warn('Pilih kategori dokumen'); - setIsLoading(true); try { + setIsSubmitting(true); // Update global state desaAntiKorupsiState.edit.form = { ...desaAntiKorupsiState.edit.form, ...formData }; @@ -116,16 +145,16 @@ export default function EditDesaAntiKorupsi() { console.error(err); toast.error('Terjadi kesalahan saat memperbarui data'); } finally { - setIsLoading(false); + setIsSubmitting(false); } }; return ( - + Edit Desa Anti Korupsi @@ -204,7 +233,7 @@ export default function EditDesaAntiKorupsi() { {previewFile && ( - + Pratinjau Dokumen @@ -219,23 +248,52 @@ export default function EditDesaAntiKorupsi() { > - - - - - - - ) - })} + + + + {data.map((v, k) => ( + + + + {v.nama} + + + Alamat: {v.alamat} + + + Jarak: {v.jarakKeDesa} + + + Telepon: {v.nomorTelepon} + + + Jam Operasional: {v.jamOperasional} + + + + + + + + + + ))} +
load(newPage)} // ini penting! + onChange={(newPage) => load(newPage)} total={totalPages} mt="md" mb="md" @@ -99,4 +126,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/component/edukasiCard.tsx b/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/component/edukasiCard.tsx index 3e5c1d5b..de2af2fe 100644 --- a/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/component/edukasiCard.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/component/edukasiCard.tsx @@ -1,7 +1,7 @@ -// Create a new component: components/EdukasiCard.tsx +// components/EdukasiCard.tsx 'use client'; -import { Box, Paper, Stack, Text } from '@mantine/core'; +import { Box, Paper, Stack, Text, Title } from '@mantine/core'; import { ReactNode } from 'react'; interface EdukasiCardProps { @@ -18,7 +18,7 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu radius="md" shadow="sm" withBorder - style={{ + style={{ height: '100%', transition: 'transform 0.2s, box-shadow 0.2s', '&:hover': { @@ -31,32 +31,35 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu {icon} - + </Stack> - <Text - size="sm" - pl={20} - style={{ - wordBreak: 'break-word', - lineHeight: 1.6, - color: 'var(--mantine-color-gray-7)' - }} - dangerouslySetInnerHTML={{ __html: description }} - /> + <Box pl={20}> + <Text + fz={{ base: 'sm', md: 'md' }} + lh={1.5} + c="gray.7" + ta="justify" + style={{ + wordBreak: 'break-word' + }} + dangerouslySetInnerHTML={{ __html: description }} + /> + </Box> </Box> </Stack> </Paper> diff --git a/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/page.tsx b/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/page.tsx index 14a4a925..9074daaa 100644 --- a/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/edukasi-lingkungan/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Box, Container, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; +import { Box, Container, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react'; import { useProxy } from 'valtio/utils'; @@ -51,19 +51,21 @@ export default function EdukasiLingkunganPage() { </Box> <Container size="lg" ta="center"> - <Text - component="h1" - fz={{ base: 'h2', md: '2.5rem' }} + <Title + order={1} c={colors['blue-button']} fw={700} mb="md" + lh={1.15} > Edukasi Lingkungan - </Text> + Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam, meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama. diff --git a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/[id]/page.tsx b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/[id]/page.tsx index fc930270..6c032d55 100644 --- a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/[kategori]/[id]/page.tsx @@ -41,23 +41,23 @@ function DetailKegiatanDesaUser() { shadow="sm" maw={900} mx="auto" - > + > - {data.image?.link && ( - {data.judul - )} {/* Judul */} - + <Title order={1} ta={"center"} c={colors['blue-button']}> {data.judul || 'Kegiatan Desa'} + {data.image?.link && ( + {data.judul + )} {/* Meta Info */} diff --git a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/layout.tsx b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/layout.tsx index ff7ea037..933d5b47 100644 --- a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/layout.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/layout.tsx @@ -1,6 +1,10 @@ // app/desa/berita/BeritaLayoutClient.tsx 'use client' +import colors from '@/con/colors'; +import { Box } from '@mantine/core'; import dynamic from 'next/dynamic'; +import { usePathname } from 'next/navigation'; +import BackButton from '../../desa/layanan/_com/BackButto'; const LayoutTabsGotongRoyong = dynamic( () => import('./_lib/layoutTabs'), @@ -8,5 +12,21 @@ const LayoutTabsGotongRoyong = dynamic( ); export default function GotongRoyongLayoutClient({ children }: { children: React.ReactNode }) { + const pathname = usePathname() + const segments = pathname.split('/').filter(Boolean) + const isDetailPage = segments.length === 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + + + + {children} + + ); + } + return {children}; } \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/semua/page.tsx b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/semua/page.tsx index 785e5b92..beb8f4e4 100644 --- a/src/app/darmasaba/(pages)/lingkungan/gotong-royong/semua/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/gotong-royong/semua/page.tsx @@ -11,7 +11,6 @@ import { Center, Container, Divider, - Flex, Grid, GridCol, Group, @@ -23,7 +22,7 @@ import { Stack, Text, Title, - Transition, + Transition } from '@mantine/core'; import { IconArrowRight, IconCalendar } from '@tabler/icons-react'; import { motion } from 'framer-motion'; @@ -46,7 +45,7 @@ export default function Page() { // Load featured data once on component mount useEffect(() => { let mounted = true; - + const loadFeatured = async () => { try { if (!featured.data && !loadingFeatured) { @@ -68,7 +67,7 @@ export default function Page() { useEffect(() => { let mounted = true; - + const loadData = async () => { try { const limit = 3; @@ -92,7 +91,7 @@ export default function Page() { if (search) url.set('search', search); if (newPage > 1) url.set('page', newPage.toString()); else url.delete('page'); - + // Use push instead of replace to keep browser history router.push(`?${url.toString()}`, { scroll: false }); }; @@ -139,7 +138,6 @@ export default function Page() { height={400} fit="cover" radius="md" - style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }} loading="lazy" /> @@ -222,6 +220,7 @@ export default function Page() { alt={item.judul} fit="cover" loading="lazy" + radius={"md"} /> @@ -241,14 +240,17 @@ export default function Page() { dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} /> - - - {new Date(item.createdAt).toLocaleDateString('id-ID', { - day: 'numeric', - month: 'short', - year: 'numeric', - })} - + + + + + {new Date(item.createdAt).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + })} + + - + ))} diff --git a/src/app/darmasaba/(pages)/lingkungan/konservasi-adat-bali/page.tsx b/src/app/darmasaba/(pages)/lingkungan/konservasi-adat-bali/page.tsx index 76a39cf6..c8b87968 100644 --- a/src/app/darmasaba/(pages)/lingkungan/konservasi-adat-bali/page.tsx +++ b/src/app/darmasaba/(pages)/lingkungan/konservasi-adat-bali/page.tsx @@ -1,11 +1,12 @@ 'use client' import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali'; import colors from '@/con/colors'; -import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; +import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; + function Page() { const filosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita.findById) const nilai = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat.findById) @@ -30,11 +31,24 @@ function Page() { - - + + Konservasi Adat Bali - </Text> - <Text px={20} ta="center" fz="lg" c="black"> + + Pelestarian lingkungan di Bali yang berpijak pada kearifan lokal, menjaga harmoni antara alam, budaya, dan manusia. @@ -54,53 +68,31 @@ function Page() { >
- + {filosofi.data?.judul} - </Text> - </Center> - <div - style={{ - wordBreak: "break-word", - whiteSpace: "normal", - flexGrow: 1 - }} - dangerouslySetInnerHTML={{ __html: filosofi.data?.deskripsi || '' }} - /> - </Stack> - </Paper> - </Box> - {/* Nilai */} - <Box style={{ display: 'flex', height: '100%' }}> - <Paper - p="lg" - style={{ - borderRadius: 16, - width: '100%', - display: 'flex', - flexDirection: 'column', - boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)' - }} - > - <Stack gap="md" px={20} style={{ height: '100%' }}> - <Center> - <Text fz="xl" fw="bold" c="black"> - {nilai.data?.judul} - </Text> +
- - {/* Bentuk */} - + + {/* Nilai */} +
- - {bentuk.data?.judul} - + + {nilai.data?.judul} +
+ + + + {/* Bentuk */} + + + +
+ + {bentuk.data?.judul} + +
+
- + ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx index 89db8ad0..c408b5b1 100644 --- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx @@ -5,6 +5,7 @@ import { Button, Container, Divider, + Flex, Group, Modal, Paper, @@ -23,11 +24,11 @@ import { useProxy } from 'valtio/utils'; import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa'; import colors from '@/con/colors'; - export default function BeasiswaPage() { const router = useRouter(); - const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar) + const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar); const [opened, { open, close }] = useDisclosure(false); + const resetForm = () => { beasiswaDesa.create.form = { namaLengkap: "", @@ -61,6 +62,7 @@ export default function BeasiswaPage() { leftSection={} onClick={() => router.back()} mb="lg" + style={{ fontSize: '1rem', fontWeight: 500 }} > Kembali @@ -69,11 +71,18 @@ export default function BeasiswaPage() { {/* Hero Section */} - + - Program Beasiswa Pendidikan Desa Darmasaba - - + + Program Beasiswa Pendidikan Desa Darmasaba + + + Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan. @@ -84,20 +93,22 @@ export default function BeasiswaPage() { - Tentang Program + + Tentang Program + - + Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih. - {/* Tambahkan info tahun berjalan di sini */} + {/* Periode Beasiswa */} - + Periode Beasiswa Tahun 2025 - + Pendaftaran beasiswa dibuka mulai 1 Januari 2025 dan ditutup pada 31 Mei 2025. Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba. @@ -108,27 +119,35 @@ export default function BeasiswaPage() { - Syarat Pendaftaran + + Syarat Pendaftaran + - Domisili Desa Darmasaba - + + Domisili Desa Darmasaba + + Peserta harus merupakan warga desa yang berdomisili minimal 2 tahun. - Nilai Akademik - + + Nilai Akademik + + Rata-rata nilai raport minimal 80 atau setara. - Surat Rekomendasi - + + Surat Rekomendasi + + Diperlukan surat rekomendasi dari sekolah atau guru wali kelas. @@ -139,75 +158,102 @@ export default function BeasiswaPage() { - Proses Seleksi + + Proses Seleksi + - - + + Pendaftaran Online + + } + > + Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung. - + Estimasi waktu: 1 Februari – 31 Mei 2025 - - + + Seleksi Administrasi + + } + > + Panitia memverifikasi kelengkapan dan validitas berkas. - + Estimasi waktu: 5–7 hari kerja setelah penutupan pendaftaran - - + + Wawancara dan Penilaian + + } + > + Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi. - + Estimasi waktu: 7–10 hari kerja setelah pengumuman seleksi administrasi - - + + Pengumuman Penerima + + } + > + Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba. - + Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai - + Total estimasi keseluruhan proses: sekitar 3–4 minggu setelah penutupan pendaftaran - {/* Testimoni */} - Cerita Sukses Penerima Beasiswa + + Cerita Sukses Penerima Beasiswa + - + “Program ini sangat membantu saya melanjutkan kuliah di Universitas Udayana. Terima kasih Desa Darmasaba!” - + – Ni Kadek Ayu S., Penerima Beasiswa 2024 - + “Selain bantuan dana, kami juga mendapatkan pelatihan komputer dan bahasa Inggris.” - + – I Made Gede A., Penerima Beasiswa 2023 @@ -218,16 +264,25 @@ export default function BeasiswaPage() { - Siap Bergabung dengan Program Ini? + + Siap Bergabung dengan Program Ini? + - + Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba. - + {/* Modal Formulir */} + Formulir Beasiswa } @@ -245,64 +300,105 @@ export default function BeasiswaPage() { { beasiswaDesa.create.form.namaLengkap = val.target.value }} /> + labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }} + onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} + /> { beasiswaDesa.create.form.nis = val.target.value }} /> + labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }} + onChange={(val) => { beasiswaDesa.create.form.nis = val.target.value }} + /> { beasiswaDesa.create.form.kelas = val.target.value }} /> + labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }} + onChange={(val) => { beasiswaDesa.create.form.kelas = val.target.value }} + /> { - stategrafikBerdasarkanResponden.create.form.ratingId = val ?? ""; - }} - data={ - (indeksKepuasanState.pilihanRatingResponden.findMany.data || []) - .filter(Boolean) // Hapus null, undefined, dll - .map((item) => ({ - value: item.id, - label: item.name || 'Tanpa Nama', - })) - } - disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading} - /> + label={"Rating"} + placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'} + value={stategrafikBerdasarkanResponden.create.form.ratingId || ""} + onChange={(val) => { + stategrafikBerdasarkanResponden.create.form.ratingId = val ?? ""; + }} + data={ + (indeksKepuasanState.pilihanRatingResponden.findMany.data || []) + .filter(Boolean) // Hapus null, undefined, dll + .map((item) => ({ + value: item.id, + label: item.name || 'Tanpa Nama', + })) + } + disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading} + /> ({ value: item.tahun, label: item.tahun }))} + value={selectedYear} + onChange={(value) => setSelectedYear(value || null)} + size="sm" + radius="md" + /> + + + + + + + } /> + + + + + + - const statePersentase = useProxy(persentasekelahiran); - const [chartData, setChartData] = useState([]); - const [selectedYear, setSelectedYear] = useState(null); + {selectedYearData && ( + + + + Rincian Tahun {selectedYear} + + + {formatNumber(selectedYearData.totalKelahiran)} kelahiran + + + {formatNumber(selectedYearData.totalKematian)} kematian + + + {/* Desktop: Table */} + + + + + Bulan + Kelahiran + Kematian + + + + {selectedYearData.data.length > 0 ? ( + <> + {selectedYearData.data.map((item) => ( + + {item.bulan} + + {formatNumber(item.kelahiran)} + + + {formatNumber(item.kematian)} + + + ))} + + Total + + {formatNumber(selectedYearData.totalKelahiran)} + + + {formatNumber(selectedYearData.totalKematian)} + + + + ) : ( + + + Tidak ada rincian bulanan + + + )} + +
+
- const formatNumber = (num: number) => new Intl.NumberFormat('id-ID').format(num); + {/* Mobile: Card List */} + + + {selectedYearData.data.length > 0 ? ( + selectedYearData.data.map((item) => ( + + Bulan + + {item.bulan} + + Kelahiran + + {formatNumber(item.kelahiran)} + + Kematian + + {formatNumber(item.kematian)} + + + )) + ) : ( +
+ Tidak ada rincian bulanan +
+ )} - - const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { - if (active && payload && payload.length) { - return ( - - Tahun {label} - Kelahiran: {formatNumber(payload[0].value)} - Kematian: {formatNumber(payload[1].value)} - - ); - } - return null; - }; - - - useShallowEffect(() => { - statePersentase.kelahiran.findMany.load(1, 1000); - statePersentase.kematian.findMany.load(1, 1000); - }, []); - - - useEffect(() => { - if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) { - const hasil = countByYearAndMonth( - statePersentase.kelahiran.findMany.data, - statePersentase.kematian.findMany.data - ); - - - setChartData(hasil); - setSelectedYear(hasil[0]?.tahun || null); - } - }, [statePersentase.kelahiran.findMany.data, statePersentase.kematian.findMany.data]); - - - if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) { - return ; - } - - - const selectedYearData = chartData.find(d => d.tahun === selectedYear); - - - return ( - - - - Statistik Kelahiran & Kematian - - - router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran')}> - - - - - router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian')}> - - - - - - - - {chartData.length === 0 ? ( -
- Belum ada data untuk ditampilkan -
- ) : ( - <> - - r.id !== "0" && r.id !== "1") // ❌ Sembunyikan SUPERADMIN dan DEVELOPER + .filter(r => r.id !== "0" && r.id !== "1") .map(r => ({ label: r.name, value: r.id, @@ -193,8 +201,6 @@ function ListUser({ search }: { search: string }) { value={item.roleId} onChange={(val) => { if (!val) return; - - // ✅ Panggil handleRoleChange dengan konfirmasi handleRoleChange( item.id, item.username, @@ -206,10 +212,16 @@ function ListUser({ search }: { search: string }) { clearable={false} nothingFoundMessage="Role tidak ditemukan" disabled={stateUser.update.loading} + styles={{ + input: { + fontSize: '1rem', + fontWeight: 500, + lineHeight: 1.5, + } + }} /> - - + handleToggleActive(item.id, item.isActive)} disabled={stateUser.update.loading} + size="compact-sm" > - {item.isActive ? : } + {item.isActive ? : } - - + @@ -243,8 +256,10 @@ function ListUser({ search }: { search: string }) { ) : ( -
- Tidak ada data user yang cocok +
+ + Tidak ada data user yang cocok +
@@ -252,6 +267,104 @@ function ListUser({ search }: { search: string }) { + + {/* Mobile Card View */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama User + + {item.username} + + + + Nomor + + {item.nomor} + + + + Role + {label}} + value={value} + onChange={(val) => onChange(val || '')} + data={options} + placeholder={placeholder} + disabled={loading} + clearable + searchable + required + radius="md" + error={error} + /> + ); + return ( @@ -108,72 +172,72 @@ function EditResponden() { label="Nama Responden" placeholder="Masukkan nama responden" value={formData.name} - onChange={handleChange('name')} + onChange={(e) => setFormData({ ...formData, name: e.currentTarget.value })} radius="md" required /> + setFormData({ ...formData, tanggal: e.currentTarget.value })} radius="md" required /> - setFormData({ ...formData, ratingId: val })} + options={(indeksKepuasanState.pilihanRatingResponden.findMany.data || []) + .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))} + loading={indeksKepuasanState.pilihanRatingResponden.findMany.loading} + error={!formData.ratingId ? 'Pilih rating' : undefined} /> - handleChange("layananPolsekId", val || "")} - /> - - - {/* List layanan */} - - Daftar Layanan Polsek - - {layananOptions.map((item) => ( - - - {item.label} - - - - - - - ))} - - {/* Submit */} - - {/* Tombol Batal */} - - - {/* Tombol Simpan */} - - - - - - ); -} - -export default EditPolsekTerdekat; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/_com/layoutPolsek.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/_com/layoutPolsek.tsx new file mode 100644 index 00000000..f415c771 --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/_com/layoutPolsek.tsx @@ -0,0 +1,150 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { IconBuilding, IconTool } from '@tabler/icons-react'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; + +function LayoutPolsek({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + + const tabs = [ + { + label: "Daftar Polsek Terdekat", + value: "daftar-polsek-terdekat", + href: "/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat", + icon: + }, + { + label: "Layanan Polsek", + value: "layanan-polsek", + href: "/admin/keamanan/polsek-terdekat/layanan-polsek", + icon: + } + ]; + + const currentTab = tabs.find(tab => tab.href === pathname); + const [activeTab, setActiveTab] = useState(currentTab?.value || tabs[0].value); + + const handleTabChange = (value: string | null) => { + const tab = tabs.find(t => t.value === value); + if (tab) { + router.push(tab.href); + } + setActiveTab(value); + }; + + useEffect(() => { + const match = tabs.find(tab => tab.href === pathname); + if (match) { + setActiveTab(match.value); + } + }, [pathname]); + + return ( + + + Polsek Terdekat + + + {/* ✅ Scroll horizontal wrapper */} + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + + {tabs.map((tab, i) => ( + + {children} + + ))} + + + ); +} + +export default LayoutPolsek; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx new file mode 100644 index 00000000..1c2b79d4 --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx @@ -0,0 +1,279 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import statePolsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat"; +import colors from "@/con/colors"; +import { + Box, + Button, + Group, + Loader, + MultiSelect, + Paper, + Stack, + TextInput, + Title +} from "@mantine/core"; +import { IconArrowBack } from "@tabler/icons-react"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { useProxy } from "valtio/utils"; + + +type FormData = { + nama: string; + jarakKeDesa: string; + alamat: string; + nomorTelepon: string; + jamOperasional: string; + embedMapUrl: string; + namaTempatMaps: string; + alamatMaps: string; + linkPetunjukArah: string; + layananPolsekId: string[]; +}; + +function EditPolsekTerdekat() { + const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState); + const params = useParams(); + const router = useRouter(); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + nama: "", + jarakKeDesa: "", + alamat: "", + nomorTelepon: "", + jamOperasional: "", + embedMapUrl: "", + namaTempatMaps: "", + alamatMaps: "", + linkPetunjukArah: "", + layananPolsekId: [] + }); + + const [originalData, setOriginalData] = useState({ + nama: "", + jarakKeDesa: "", + alamat: "", + nomorTelepon: "", + jamOperasional: "", + embedMapUrl: "", + namaTempatMaps: "", + alamatMaps: "", + linkPetunjukArah: "", + layananPolsekId: [] + }); + + useEffect(() => { + statePolsekTerdekat.layananPolsek.findManyAll.load(); + }, []); + + // load data untuk form edit + useEffect(() => { + const loadPolsekTerdekat = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await polsekState.edit.load(id); + if (data) { + setFormData({ + nama: data.nama || "", + jarakKeDesa: data.jarakKeDesa || "", + alamat: data.alamat || "", + nomorTelepon: data.nomorTelepon || "", + jamOperasional: data.jamOperasional || "", + embedMapUrl: data.embedMapUrl || "", + namaTempatMaps: data.namaTempatMaps || "", + alamatMaps: data.alamatMaps || "", + linkPetunjukArah: data.linkPetunjukArah || "", + layananPolsekId: data.LayananToPolsek?.map((l: any) => l.layananId) || [], + }); + + setOriginalData({ + nama: data.nama || "", + jarakKeDesa: data.jarakKeDesa || "", + alamat: data.alamat || "", + nomorTelepon: data.nomorTelepon || "", + jamOperasional: data.jamOperasional || "", + embedMapUrl: data.embedMapUrl || "", + namaTempatMaps: data.namaTempatMaps || "", + alamatMaps: data.alamatMaps || "", + linkPetunjukArah: data.linkPetunjukArah || "", + layananPolsekId: data.LayananToPolsek?.map((l: any) => l.layananId) || [], + }); + } + } catch (error) { + console.error("Error loading polsek terdekat:", error); + toast.error("Gagal memuat data polsek terdekat"); + } + }; + + loadPolsekTerdekat(); + }, [params?.id]); + + + const handleChange = (key: keyof FormData, value: any) => { + setFormData((prev) => ({ ...prev, [key]: value })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + jarakKeDesa: originalData.jarakKeDesa, + alamat: originalData.alamat, + nomorTelepon: originalData.nomorTelepon, + jamOperasional: originalData.jamOperasional, + embedMapUrl: originalData.embedMapUrl, + namaTempatMaps: originalData.namaTempatMaps, + alamatMaps: originalData.alamatMaps, + linkPetunjukArah: originalData.linkPetunjukArah, + layananPolsekId: (originalData as any)?.LayananToPolsek?.map((l: any) => l.layananId) || [], + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + await polsekState.edit.update(); + toast.success("Polsek terdekat berhasil diperbarui!"); + router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat"); + } catch (error) { + console.error("Error updating polsek terdekat:", error); + toast.error("Gagal memperbarui data polsek terdekat"); + } finally { + setIsSubmitting(false); + } +}; + + return ( + + {/* Header */} + + + + Edit Polsek Terdekat + + + + {/* Form utama */} + + + {/* Input fields */} + handleChange("nama", e.currentTarget.value)} + label="Nama Polsek Terdekat" + placeholder="Masukkan nama Polsek Terdekat" + required + /> + handleChange("jarakKeDesa", e.currentTarget.value)} + label="Jarak Polsek Terdekat" + /> + handleChange("alamat", e.currentTarget.value)} + label="Alamat Polsek Terdekat" + /> + handleChange("nomorTelepon", e.currentTarget.value)} + label="Nomor Telepon" + /> + handleChange("jamOperasional", e.currentTarget.value)} + label="Jam Operasional" + /> + handleChange("embedMapUrl", e.currentTarget.value)} + label="Embed Map URL" + /> + handleChange("namaTempatMaps", e.currentTarget.value)} + label="Nama Tempat Maps" + /> + handleChange("alamatMaps", e.currentTarget.value)} + label="Alamat Maps" + /> + handleChange("linkPetunjukArah", e.currentTarget.value)} + label="Link Petunjuk Arah" + /> + + handleChange('layananPolsekId', val)} + data={ + statePolsekTerdekat.layananPolsek.findManyAll.data?.map((v) => ({ + value: v.id, + label: v.nama, + })) || [] + } + clearable + searchable + required + error={!formData.layananPolsekId.length ? 'Pilih minimal satu layanan polsek' : undefined} + /> + + {/* Submit */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default EditPolsekTerdekat; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/page.tsx similarity index 87% rename from src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx rename to src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/page.tsx index 71f18cc2..5d8ac062 100644 --- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/page.tsx @@ -6,12 +6,12 @@ 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 polsekTerdekat from '../../../_state/keamanan/polsek-terdekat'; +import { ModalKonfirmasiHapus } from '../../../../_com/modalKonfirmasiHapus'; +import statePolsekTerdekat from '../../../../_state/keamanan/polsek-terdekat'; function DetailPolsekTerdekat() { const router = useRouter(); - const polsekState = useProxy(polsekTerdekat); + const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState); const [selectedId, setSelectedId] = useState(null); const [modalHapus, setModalHapus] = useState(false); const params = useParams(); @@ -25,7 +25,7 @@ function DetailPolsekTerdekat() { polsekState.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - router.push("/admin/keamanan/polsek-terdekat"); + router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat"); } }; @@ -40,7 +40,7 @@ function DetailPolsekTerdekat() { const data = polsekState.findUnique.data; return ( - + {/* Tombol Back */} - - - + {/* Header */} diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx index f5d12f86..26aaad1b 100644 --- a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx @@ -54,6 +54,28 @@ function Page() { const permohonanInformasiPublikState = useProxy(statePermohonanInformasi); const router = useRouter(); + // Helper function to validate email format + const isValidEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Check if form is valid + const isFormValid = () => { + const form = permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form; + return ( + form.name?.trim() !== '' && + form.nik?.trim() !== '' && + form.notelp?.trim() !== '' && + form.alamat?.trim() !== '' && + form.email?.trim() !== '' && + isValidEmail(form.email) && + form.jenisInformasiDimintaId && + form.caraMemperolehInformasiId && + form.caraMemperolehSalinanInformasiId + ); + }; + const submitForms = async () => { const { create } = permohonanInformasiPublikState.statepermohonanInformasiPublik; const hasil = await create.create(); @@ -266,6 +288,7 @@ function Page() { bg={colors['blue-button']} leftSection={} onClick={submitForms} + disabled={!isFormValid()} > Kirim Permohonan diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx index 80de7c2e..ad60d688 100644 --- a/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx @@ -56,6 +56,24 @@ function Page() { const stateKeberatan = useProxy(permohonanKeberatanInformasi); const router = useRouter(); + // Helper function to validate email format + const isValidEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Check if form is valid + const isFormValid = () => { + const form = stateKeberatan.create.form; + return ( + form.name?.trim() !== '' && + form.email?.trim() !== '' && + isValidEmail(form.email) && + form.notelp?.trim() !== '' && + form.alasan?.trim() !== '' + ); + }; + const submit = async () => { const hasil = await stateKeberatan.create.create(); if (hasil) router.push('/darmasaba/permohonan/berhasil'); @@ -232,6 +250,7 @@ function Page() { radius="md" fw={600} bg={colors['blue-button']} + disabled={!isFormValid()} > Kirim Permohonan -- 2.49.1 From d43b07c2ef419b9b523a6d2644e3d684f8f57170 Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 18 Feb 2026 10:51:10 +0800 Subject: [PATCH 87/97] feat: add form validation for kesehatan module admin pages - Added isFormValid() and isHtmlEmpty() helper functions - Disabled submit buttons when required fields are empty - Applied consistent validation pattern across all create/edit pages - Validated fields: name, address, dates, descriptions, and image uploads - Edit pages allow existing images, create pages require new uploads Co-authored-by: Qwen-Coder --- .../artikel_kesehatan/create/page.tsx | 107 +++++++++++++++++- .../fasilitas_kesehatan/[id]/edit/page.tsx | 73 +++++++++++- .../fasilitas_kesehatan/create/page.tsx | 73 +++++++++++- .../dokter-tenaga-medis/[id]/edit/page.tsx | 59 +++++++++- .../dokter-tenaga-medis/create/page.tsx | 63 ++++++++++- .../tarif-layanan/[id]/page.tsx | 23 +++- .../tarif-layanan/create/page.tsx | 23 +++- .../jadwal_kegiatan/[id]/edit/page.tsx | 24 +++- .../jadwal_kegiatan/create/page.tsx | 24 +++- .../penderita_penyakit/[id]/edit/page.tsx | 41 ++++++- .../penderita_penyakit/create/page.tsx | 41 ++++++- .../kelahiran/[id]/edit/page.tsx | 35 +++++- .../kelahiran/create/page.tsx | 35 +++++- .../kematian/[id]/edit/page.tsx | 48 +++++++- .../kematian/create/page.tsx | 56 +++++++-- .../info-wabah-penyakit/[id]/edit/page.tsx | 21 +++- .../info-wabah-penyakit/create/page.tsx | 22 +++- .../kontak-darurat/[id]/edit/page.tsx | 21 +++- .../kesehatan/kontak-darurat/create/page.tsx | 22 +++- .../penanganan-darurat/[id]/edit/page.tsx | 20 +++- .../penanganan-darurat/create/page.tsx | 21 +++- .../kesehatan/posyandu/[id]/edit/page.tsx | 49 +++++++- .../kesehatan/posyandu/create/page.tsx | 47 +++++++- .../program-kesehatan/[id]/edit/page.tsx | 21 +++- .../program-kesehatan/create/page.tsx | 22 +++- .../kesehatan/puskesmas/[id]/edit/page.tsx | 16 ++- .../kesehatan/puskesmas/create/page.tsx | 14 ++- .../profil/media-sosial/create/page.tsx | 1 - 28 files changed, 982 insertions(+), 40 deletions(-) diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx index 354169d8..857426cf 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx @@ -30,6 +30,33 @@ function CreateArtikelKesehatan() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateArtikelKesehatan.create.form.title?.trim() !== '' && + stateArtikelKesehatan.create.form.content?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.introduction.content) && + stateArtikelKesehatan.create.form.symptom.title?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.symptom.content) && + stateArtikelKesehatan.create.form.prevention.title?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.prevention.content) && + stateArtikelKesehatan.create.form.firstAid.title?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.firstAid.content) && + stateArtikelKesehatan.create.form.mythVsFact.title?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.mitos) && + !isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.fakta) && + !isHtmlEmpty(stateArtikelKesehatan.create.form.doctorSign.content) && + file !== null + ); + }; + const resetForm = () => { stateArtikelKesehatan.create.form = { title: '', @@ -65,10 +92,79 @@ function CreateArtikelKesehatan() { const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); + + if (!stateArtikelKesehatan.create.form.title?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.content?.trim()) { + toast.error('Deskripsi wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.introduction.content)) { + toast.error('Pendahuluan wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.symptom.title?.trim()) { + toast.error('Judul gejala wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.symptom.content)) { + toast.error('Deskripsi gejala wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.prevention.title?.trim()) { + toast.error('Judul pencegahan wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.prevention.content)) { + toast.error('Deskripsi pencegahan wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.firstAid.title?.trim()) { + toast.error('Judul pertolongan pertama wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.firstAid.content)) { + toast.error('Deskripsi pertolongan pertama wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.mythVsFact.title?.trim()) { + toast.error('Judul mitos vs fakta wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.mitos)) { + toast.error('Deskripsi mitos wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.fakta)) { + toast.error('Deskripsi fakta wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.doctorSign.content)) { + toast.error('Deskripsi kapan harus ke dokter wajib diisi'); + return; + } + + if (!file) { + toast.error('Gambar wajib dipilih'); + return; + } + try { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); - } + setIsSubmitting(true); const res = await ApiFetch.api.fileStorage.create.post({ file, @@ -344,8 +440,11 @@ function CreateArtikelKesehatan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx index 32db4e6b..01e6a6ff 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx @@ -45,6 +45,28 @@ function EditFasilitasKesehatan() { const params = useParams<{ id: string }>(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.informasiUmum.fasilitas?.trim() !== '' && + formData.informasiUmum.alamat?.trim() !== '' && + formData.informasiUmum.jamOperasional?.trim() !== '' && + !isHtmlEmpty(formData.layananUnggulan.content) && + formData.dokterdanTenagaMedis.length > 0 && + !isHtmlEmpty(formData.fasilitasPendukung.content) && + !isHtmlEmpty(formData.prosedurPendaftaran.content) && + formData.tarifDanLayanan.length > 0 + ); + }; + const [formData, setFormData] = useState({ name: '', informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' }, @@ -111,6 +133,52 @@ function EditFasilitasKesehatan() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + + if (!formData.name?.trim()) { + toast.error('Nama fasilitas kesehatan wajib diisi'); + return; + } + + if (!formData.informasiUmum.fasilitas?.trim()) { + toast.error('Fasilitas wajib diisi'); + return; + } + + if (!formData.informasiUmum.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + + if (!formData.informasiUmum.jamOperasional?.trim()) { + toast.error('Jam operasional wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.layananUnggulan.content)) { + toast.error('Layanan unggulan wajib diisi'); + return; + } + + if (formData.dokterdanTenagaMedis.length === 0) { + toast.error('Dokter dan tenaga medis wajib dipilih'); + return; + } + + if (isHtmlEmpty(formData.fasilitasPendukung.content)) { + toast.error('Fasilitas pendukung wajib diisi'); + return; + } + + if (formData.tarifDanLayanan.length === 0) { + toast.error('Tarif dan layanan wajib dipilih'); + return; + } + + if (isHtmlEmpty(formData.prosedurPendaftaran.content)) { + toast.error('Prosedur pendaftaran wajib diisi'); + return; + } + try { setIsSubmitting(true); @@ -264,8 +332,11 @@ function EditFasilitasKesehatan() { diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx index 4f848982..fc26a831 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx @@ -24,6 +24,16 @@ function AdministrasiOnline() { const [opened, { open, close }] = useDisclosure(false); const state = useProxy(layananonlineDesa); + // Check if form is valid + const isFormValid = () => { + return ( + state.administrasiOnline.create.form.name?.trim() !== '' && + state.administrasiOnline.create.form.alamat?.trim() !== '' && + state.administrasiOnline.create.form.nomorTelepon?.trim() !== '' && + state.administrasiOnline.create.form.jenisLayananId?.trim() !== '' + ); + }; + useEffect(() => { // ✅ Panggil load data jenis layanan dari backend if (!state.jenisLayanan.findMany.data) { @@ -104,7 +114,11 @@ function AdministrasiOnline() { } /> - diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx index 6775bdb1..b45c6082 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx @@ -19,6 +19,28 @@ function PengaduanMasyarakat() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + state.pengaduanMasyarakat.create.form.name?.trim() !== '' && + state.pengaduanMasyarakat.create.form.email?.trim() !== '' && + state.pengaduanMasyarakat.create.form.nomorTelepon?.trim() !== '' && + state.pengaduanMasyarakat.create.form.nik?.trim() !== '' && + state.pengaduanMasyarakat.create.form.judulPengaduan?.trim() !== '' && + state.pengaduanMasyarakat.create.form.lokasiKejadian?.trim() !== '' && + !isHtmlEmpty(state.pengaduanMasyarakat.create.form.deskripsiPengaduan) && + state.pengaduanMasyarakat.create.form.jenisPengaduanId?.trim() !== '' && + file !== null + ); + }; + useEffect(() => { // ✅ Panggil load data jenis layanan dari backend if (!state.jenisPengaduan.findMany.data) { @@ -207,7 +229,11 @@ function PengaduanMasyarakat() { - diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx index 42a22ec3..de174975 100644 --- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx @@ -37,6 +37,24 @@ function Page() { }; }; + // Check if form is valid + const isFormValid = () => { + return ( + beasiswaDesa.create.form.namaLengkap?.trim() !== '' && + beasiswaDesa.create.form.nis?.trim() !== '' && + beasiswaDesa.create.form.kelas?.trim() !== '' && + beasiswaDesa.create.form.jenisKelamin?.trim() !== '' && + beasiswaDesa.create.form.alamatDomisili?.trim() !== '' && + beasiswaDesa.create.form.tempatLahir?.trim() !== '' && + beasiswaDesa.create.form.tanggalLahir?.trim() !== '' && + beasiswaDesa.create.form.namaOrtu?.trim() !== '' && + beasiswaDesa.create.form.nik?.trim() !== '' && + beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' && + beasiswaDesa.create.form.penghasilan?.trim() !== '' && + beasiswaDesa.create.form.noHp?.trim() !== '' + ); + }; + const { data, page, totalPages, loading, load } = ungggulanDesa.findMany; useShallowEffect(() => { @@ -238,7 +256,7 @@ function Page() { onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} /> - + diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx index c408b5b1..6678c274 100644 --- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx @@ -46,6 +46,24 @@ export default function BeasiswaPage() { }; }; + // Check if form is valid + const isFormValid = () => { + return ( + beasiswaDesa.create.form.namaLengkap?.trim() !== '' && + beasiswaDesa.create.form.nis?.trim() !== '' && + beasiswaDesa.create.form.kelas?.trim() !== '' && + beasiswaDesa.create.form.jenisKelamin?.trim() !== '' && + beasiswaDesa.create.form.alamatDomisili?.trim() !== '' && + beasiswaDesa.create.form.tempatLahir?.trim() !== '' && + beasiswaDesa.create.form.tanggalLahir?.trim() !== '' && + beasiswaDesa.create.form.namaOrtu?.trim() !== '' && + beasiswaDesa.create.form.nik?.trim() !== '' && + beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' && + beasiswaDesa.create.form.penghasilan?.trim() !== '' && + beasiswaDesa.create.form.noHp?.trim() !== '' + ); + }; + const handleSubmit = async () => { await beasiswaDesa.create.create(); resetForm(); @@ -391,6 +409,7 @@ export default function BeasiswaPage() { radius="xl" bg={colors['blue-button']} onClick={handleSubmit} + disabled={!isFormValid()} style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 }} > Kirim diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx index e137e07a..368d19ca 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx @@ -42,6 +42,24 @@ export default function ModalPeminjaman({ const BATAS_HARI_PINJAM = 4; + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + snap.create.form.nama?.trim() !== '' && + snap.create.form.noTelp?.trim() !== '' && + snap.create.form.alamat?.trim() !== '' && + snap.create.form.tanggalPinjam?.trim() !== '' && + !isHtmlEmpty(snap.create.form.catatan) + ); + }; + // Reset form setiap modal dibuka useEffect(() => { if (opened && buku) { @@ -222,13 +240,13 @@ export default function ModalPeminjaman({ diff --git a/src/app/admin/(dashboard)/_com/judulListTab.tsx b/src/app/admin/(dashboard)/_com/judulListTab.tsx index 21037671..dda1fe69 100644 --- a/src/app/admin/(dashboard)/_com/judulListTab.tsx +++ b/src/app/admin/(dashboard)/_com/judulListTab.tsx @@ -1,9 +1,11 @@ 'use client' -import colors from '@/con/colors'; -import { Grid, GridCol, Button, Text, Paper, TextInput } from '@mantine/core'; +import { Grid, GridCol, Button, Paper, TextInput } from '@mantine/core'; import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import React from 'react'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedText } from '@/components/admin/UnifiedTypography'; type JudulListTabProps = { title: string; @@ -14,17 +16,16 @@ type JudulListTabProps = { onChange?: (e: React.ChangeEvent) => void; } - - - const JudulListTab = ({ title = "", href = "#", placeholder = "pencarian", searchIcon = , value, - onChange + onChange }: JudulListTabProps) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); const router = useRouter(); const handleNavigate = () => { @@ -34,10 +35,17 @@ const JudulListTab = ({ return ( - {title} + + {title} + - + - diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 55687b41..6700ea9b 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,7 +1,9 @@ 'use client' -import colors from "@/con/colors"; import { authStore } from "@/store/authStore"; +import { useDarkMode } from "@/state/darkModeStore"; +import { themeTokens, getActiveStateStyles } from "@/utils/themeTokens"; +import { DarkModeToggle } from "@/components/admin/DarkModeToggle"; import { ActionIcon, AppShell, @@ -33,13 +35,21 @@ import { useEffect, useState } from "react"; import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar"; export default function Layout({ children }: { children: React.ReactNode }) { - const [opened, { toggle, close }] = useDisclosure(); // ✅ Tambahkan 'close' + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const [mounted, setMounted] = useState(false); + const [opened, { toggle, close }] = useDisclosure(); const [loading, setLoading] = useState(true); const [isLoggingOut, setIsLoggingOut] = useState(false); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); const router = useRouter(); const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s)); - + + // Ensure component is mounted on client side + useEffect(() => { + setMounted(true); + }, []); useEffect(() => { const fetchUser = async () => { @@ -74,7 +84,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { }); const currentPath = window.location.pathname; - + if (currentPath === '/admin') { const expectedPath = getRedirectPath(Number(data.user.roleId)); console.log('🔄 Redirecting from /admin to:', expectedPath); @@ -112,11 +122,11 @@ export default function Layout({ children }: { children: React.ReactNode }) { } }; - if (loading) { + if (loading || !mounted) { return ( -
+
@@ -132,7 +142,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { try { setIsLoggingOut(true); - const response = await fetch('/api/auth/logout', { + const response = await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); @@ -158,10 +168,9 @@ export default function Layout({ children }: { children: React.ReactNode }) { } }; - // ✅ Handler untuk menutup mobile menu saat navigasi const handleNavClick = (path: string) => { router.push(path); - close(); // Tutup mobile menu + close(); }; return ( @@ -178,11 +187,16 @@ export default function Layout({ children }: { children: React.ReactNode }) { }} padding="md" > + {/* + HEADER / TOPBAR + Spec: Background gradient, border bawah wajib + */} - + Admin Darmasaba + {/* Dark Mode Toggle */} + + {!desktopOpened && ( - + )} - + - router.push("/darmasaba")} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }}> + router.push("/darmasaba")} + color={mounted ? tokens.colors.primary : '#3B82F6'} + radius="xl" + size="lg" + variant="gradient" + gradient={mounted ? tokens.colors.gradient : { from: '#3B82F6', to: '#60A5FA' }} + > Logo Darmasaba - + @@ -229,47 +262,104 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + {/* + SIDEBAR / NAVBAR + Spec: Background --bg-app, active state dengan accent bar + */} + {currentNav.map((v, k) => { const isParentActive = segments.includes(_.lowerCase(v.name)); return ( - {v.name}} - style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} - styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} - variant="light" + + {v.name} + + } + style={{ + borderRadius: rem(10), + marginBottom: rem(4), + transition: "background 150ms ease", + ...(mounted && isParentActive && !isDark && { + borderLeft: `3px solid ${tokens.colors.primary}`, + }), + }} + styles={{ + root: { + '&:hover': { + backgroundColor: mounted && isDark ? '#1E293B' : tokens.colors.bg.hover, + }, + ...(mounted && isParentActive && isDark && { + backgroundColor: 'rgba(59,130,246,0.25)', + borderLeft: `3px solid ${tokens.colors.primary}`, + }), + } + }} + variant="light" active={isParentActive} > {v.children.map((child, key) => { const isChildActive = segments.includes(_.lowerCase(child.name)); return ( - { e.preventDefault(); handleNavClick(child.path); }} href={child.path} - c={isChildActive ? colors["blue-button"] : "gray"} - label={{child.name}} - styles={{ - root: { - borderRadius: rem(8), - marginBottom: rem(2), - transition: 'background 150ms ease', - padding: '6px 12px', - '&:hover': { - backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' - }, - ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) - } - }} - active={isChildActive} + c={mounted && isChildActive ? tokens.colors.primary : mounted && isDark ? '#E5E7EB' : tokens.colors.text.secondary} + label={ + + {child.name} + + } + styles={{ + root: { + borderRadius: rem(8), + marginBottom: rem(2), + transition: 'background 150ms ease', + padding: '6px 12px', + '&:hover': { + backgroundColor: mounted && isDark ? 'rgba(255, 255, 255, 0.05)' : tokens.colors.bg.hover, + }, + ...(mounted && isChildActive && isDark && { + backgroundColor: 'rgba(59,130,246,0.15)', + borderLeft: `2px solid ${tokens.colors.primary}`, + }), + ...(mounted && isChildActive && !isDark && { + backgroundColor: tokens.colors.bg.hover, + }), + } + }} + active={isChildActive} + variant="subtle" component={Link} /> ); @@ -282,7 +372,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + @@ -290,7 +380,17 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + {/* + MAIN CONTENT + Spec: Background --bg-base + */} + {children} diff --git a/src/components/admin/AdminThemeProvider.tsx b/src/components/admin/AdminThemeProvider.tsx new file mode 100644 index 00000000..0a2a5c76 --- /dev/null +++ b/src/components/admin/AdminThemeProvider.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { MantineProvider, createTheme } from '@mantine/core'; +import '@mantine/core/styles.css'; +import '@/styles/dark-mode-table.css'; +import React from 'react'; + +/** + * Admin Theme Provider + * + * Wrapper untuk MantineProvider dengan custom theme + * Mendukung dark mode otomatis + * + * Usage: + * import { AdminThemeProvider } from '@/components/admin/AdminThemeProvider'; + * + * + * + * + */ + +interface AdminThemeProviderProps { + children: React.ReactNode; + forceTheme?: 'light' | 'dark'; +} + +export function AdminThemeProvider({ children, forceTheme }: AdminThemeProviderProps) { + const { isDark } = useDarkMode(); + + // Use forced theme if provided, otherwise use store + const useDark = forceTheme ? forceTheme === 'dark' : isDark; + const tokens = themeTokens(useDark); + + const theme = createTheme({ + colors: { + primary: [ + tokens.colors.primaryLight, + tokens.colors.primaryLight, + tokens.colors.primary, + tokens.colors.primary, + tokens.colors.primary, + tokens.colors.primary, + tokens.colors.primaryDark, + tokens.colors.primaryDark, + tokens.colors.primaryDark, + tokens.colors.primaryDark, + ], + }, + primaryColor: 'primary', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + fontFamilyMonospace: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace', + + // Override default colors based on mode + white: tokens.colors.text.inverse, + black: tokens.colors.text.primary, + + // CSS variables for table hover + activeClassName: useDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)', + + // Component defaults + components: { + Paper: { + defaultProps: { + bg: tokens.colors.bg.card, + radius: 'md', + shadow: 'sm', + }, + }, + Button: { + defaultProps: { + radius: 'md', + }, + }, + TextInput: { + defaultProps: { + radius: 'md', + }, + }, + Select: { + defaultProps: { + radius: 'md', + }, + }, + Modal: { + defaultProps: { + radius: 'lg', + }, + }, + Table: { + defaultProps: { + highlightOnHover: true, + }, + }, + }, + }); + + return ( + +
+ {children} +
+
+ ); +} + +export default AdminThemeProvider; diff --git a/src/components/admin/DarkModeToggle.tsx b/src/components/admin/DarkModeToggle.tsx new file mode 100644 index 00000000..9ef35ecb --- /dev/null +++ b/src/components/admin/DarkModeToggle.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { ActionIcon, Tooltip, Transition } from '@mantine/core'; +import { IconMoon, IconSun } from '@tabler/icons-react'; + +/** + * Dark Mode Toggle Button + * + * Component untuk toggle dark/light mode + * + * Usage: + * import { DarkModeToggle } from '@/components/admin/DarkModeToggle'; + * + * + */ + +interface DarkModeToggleProps { + variant?: 'light' | 'filled' | 'outline' | 'subtle'; + size?: 'sm' | 'md' | 'lg'; + color?: string; + showTooltip?: boolean; + tooltipPosition?: 'top' | 'bottom' | 'left' | 'right'; +} + +export function DarkModeToggle({ + variant = 'light', + size = 'lg', + color, + showTooltip = true, + tooltipPosition = 'bottom', +}: DarkModeToggleProps) { + const { isDark, toggle } = useDarkMode(); + const tokens = themeTokens(isDark); + + const iconColor = color || tokens.colors.primary; + + return ( + + + {/* Icon Sun untuk Light Mode */} + + {(style) => ( + + )} + + + {/* Icon Moon untuk Dark Mode */} + + {(style) => ( + + )} + + + + ); +} + +export default DarkModeToggle; diff --git a/src/components/admin/README_UNIFIED_STYLING.md b/src/components/admin/README_UNIFIED_STYLING.md new file mode 100644 index 00000000..3e8375f1 --- /dev/null +++ b/src/components/admin/README_UNIFIED_STYLING.md @@ -0,0 +1,546 @@ +# 🎨 Unified Styling System - Admin Dashboard + +Sistem styling terpusat untuk admin dashboard Darmasaba dengan dukungan **dark mode**. + +**Berdasarkan spesifikasi:** `darkMode.md` + +--- + +## 📋 Daftar Isi + +- [Konsep Utama](#konsep-utama) +- [Dark Mode Palette](#dark-mode-palette) +- [Struktur File](#struktur-file) +- [Cara Menggunakan](#cara-menggunakan) +- [Mengedit Style](#mengedit-style) +- [Dark Mode Toggle](#dark-mode-toggle) +- [Contoh Penggunaan](#contoh-penggunaan) + +--- + +## 🎯 Konsep Utama + +**Satu File Edit = Semua Halaman Terupdate** + +Sebelumnya: +- ❌ Style tersebar di 493 file `.tsx` +- ❌ Hardcode warna di setiap komponen +- ❌ Tidak ada konsistensi +- ❌ Sulit maintain + +Sekarang: +- ✅ Edit di **1 file** = semua halaman update +- ✅ Component reusable +- ✅ Konsisten di seluruh aplikasi +- ✅ Dark mode otomatis sesuai spesifikasi `darkMode.md` + +--- + +## 🌙 Dark Mode Palette + +### Background Layers (Dark Mode) +| Layer | Token | Warna | Fungsi | +|------|------|------|------| +| Base | `bg.base` | `#0B1220` | Background utama aplikasi | +| App | `bg.app` | `#0F172A` | Area sidebar | +| Card | `bg.card` | `#162235` | Card / container | +| Surface | `bg.surface` | `#1E2A3D` | Table header, tab, input | + +### Text Colors (Dark Mode) +| Jenis | Token | Warna | +|-----|------|------| +| Primary | `text.primary` | `#E5E7EB` | +| Secondary | `text.secondary` | `#9CA3AF` | +| Muted | `text.muted` | `#6B7280` | + +### Accent & Actions (Dark Mode) +| Fungsi | Warna | +|------|------| +| Primary Action | `#3B82F6` | +| Hover | `#2563EB` | +| Active | `#1D4ED8` | +| Link | `#60A5FA` | + +### Borders (Dark Mode) +| Token | Warna | +|-----|------| +| `border.default` | `#2A3A52` | +| `border.soft` | `#22314A` | + +> **Catatan:** Light mode menggunakan palette original yang lebih terang + +--- + +## 📁 Struktur File + +``` +src/ +├── utils/ +│ └── themeTokens.ts # 📦 PUSAT SEMUA STYLE (edit di sini!) +├── state/ +│ └── darkModeStore.ts # 🌙 State management dark mode +├── components/admin/ +│ ├── DarkModeToggle.tsx # 🌓 Toggle button +│ ├── AdminThemeProvider.tsx # 🎨 Theme provider wrapper +│ ├── UnifiedTypography.tsx # 📝 Text components (Title, Text) +│ ├── UnifiedSurface.tsx # 📦 Card, Paper components +│ └── README_UNIFIED_STYLING.md # 📖 Dokumentasi ini +├── app/admin/ +│ ├── layout.tsx # ✅ Sudah diupdate dengan dark mode +│ └── (dashboard)/ +│ └── _com/ +│ ├── header.tsx # ✅ Sudah diupdate +│ ├── judulList.tsx # ✅ Sudah diupdate +│ └── judulListTab.tsx # ✅ Sudah diupdate +└── darkMode.md # 📐 Spesifikasi lengkap dark mode +``` + +--- + +## 🚀 Cara Menggunakan + +### 1. **Untuk Developer: Edit Style Global** + +Edit file: `src/utils/themeTokens.ts` + +```typescript +export const themeTokens = (isDark: boolean = false): ThemeTokens => { + const darkColors = { + bgBase: '#0B1220', // ← Edit warna dark mode di sini + bgCard: '#162235', + textPrimary: '#E5E7EB', + primaryAction: '#3B82F6', + // ... dan lainnya + }; + + return { + colors: { + primary: current.primaryAction, + bg: { + base: current.bgBase, + card: current.bgCard, + // ... + }, + // ... + }, + }; +}; +``` + +### 2. **Menggunakan Components di Halaman** + +#### A. Typography Components + +```tsx +import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography'; + +// Heading - otomatis dark mode +Judul Halaman +Sub Judul +Section Title +Card Title + +// Text dengan color semantic +Teks primary +Teks secondary +Teks muted +Link text +Brand color + +// Dengan weight +Teks bold +Teks medium +``` + +#### B. Surface Components + +```tsx +import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface'; + +// Card sederhana - border dan warna otomatis dark mode + +

Isi card

+
+ +// Card dengan sections + + + Header + + + +

Body content

+
+ + + + +
+ +// Divider dengan variant + {/* Default */} + + +``` + +#### C. Page Header Component + +```tsx +import { UnifiedPageHeader } from '@/components/admin/UnifiedTypography'; + + + Tambah Baru + + } +/> +``` + +### 3. **Menggunakan Theme Tokens Langsung** + +```tsx +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; + +function MyComponent() { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( +
+

+ Konten dengan styling konsisten +

+
+ ); +} +``` + +--- + +## 🌓 Dark Mode Toggle + +### Otomatis di Header + +Dark mode toggle sudah terintegrasi di header admin dashboard. User bisa toggle dengan klik tombol 🌙/☀️. + +### Manual Toggle + +```tsx +import { useDarkMode } from '@/state/darkModeStore'; +import { DarkModeToggle } from '@/components/admin/DarkModeToggle'; + +function MyComponent() { + const { isDark, toggle } = useDarkMode(); + + return ( +
+

Current mode: {isDark ? 'Dark' : 'Light'}

+ + {/* Gunakan component toggle */} + + + {/* Atau manual */} + +
+ ); +} +``` + +### Persistensi + +Dark mode preference disimpan di `localStorage` dengan key `darmasaba-admin-dark-mode`. +Preference akan tetap ada saat user refresh halaman atau kembali nanti. + +--- + +## 📝 Contoh Penggunaan Lengkap + +### Contoh 1: List Page dengan Table + +```tsx +'use client' +import { UnifiedPageHeader, UnifiedText } from '@/components/admin/UnifiedTypography'; +import UnifiedCard from '@/components/admin/UnifiedSurface'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { Button, Table, TableTr, TableTh, TableTd } from '@mantine/core'; + +export default function DaftarBerita() { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( +
+ {/* Header Halaman */} + + + Tambah Berita + + } + /> + + {/* Card untuk Table */} + + + + + + Judul + + + Kategori + + + + + {data.map((item) => ( + + + {item.judul} + + + + {item.kategori} + + + + ))} + +
+
+
+ ); +} +``` + +### Contoh 2: Detail Page + +```tsx +import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography'; +import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface'; + +export default function DetailBerita({ data }) { + return ( + + + {data.judul} + + + + + Kategori + {data.kategori} + + + + + + Deskripsi + {data.deskripsi} + + + + + + Konten +
+ + + + + + + + + + + ); +} +``` + +--- + +## 🎨 Mengedit Style + +### Edit Warna Dark Mode + +File: `src/utils/themeTokens.ts` + +```typescript +const darkColors = { + // Background Layers + bgBase: '#0B1220', // ← Edit di sini + bgApp: '#0F172A', + bgCard: '#162235', + bgSurface: '#1E2A3D', + + // Text + textPrimary: '#E5E7EB', // ← Edit di sini + textSecondary: '#9CA3AF', + + // Accent + primaryAction: '#3B82F6', // ← Edit primary color +}; +``` + +### Edit Warna Light Mode + +```typescript +const lightColors = { + bgBase: '#f6f9fc', + bgCard: '#ffffff', + textPrimary: '#1a1b1e', + primaryAction: baseColors['blue-button'], // Dari colors.ts +}; +``` + +### Edit Typography + +```typescript +typography: { + h1: { + fz: '2rem', // ← Edit ukuran + fw: 700, // ← Edit weight + lh: 1.2, // ← Edit line height + }, + body: { + fz: '1rem', + fw: 400, + lh: 1.5, + }, +} +``` + +### Edit Spacing & Radius + +```typescript +spacing: { + xs: '0.625rem', // 10px + sm: '1rem', // 16px + md: '1.5rem', // 24px + lg: '2rem', // 32px +} + +radius: { + sm: '0.5rem', // 8px + md: '0.75rem', // 12px + lg: '1rem', // 16px +} +``` + +--- + +## ✅ Checklist Migrasi + +Komponen yang sudah diupdate dengan dark mode: + +- ✅ `src/app/admin/layout.tsx` +- ✅ `src/app/admin/(dashboard)/_com/header.tsx` +- ✅ `src/app/admin/(dashboard)/_com/judulList.tsx` +- ✅ `src/app/admin/(dashboard)/_com/judulListTab.tsx` +- ✅ `src/components/admin/UnifiedTypography.tsx` +- ✅ `src/components/admin/UnifiedSurface.tsx` +- ✅ `src/components/admin/DarkModeToggle.tsx` +- ✅ `src/utils/themeTokens.ts` + +Komponen yang perlu diupdate (TODO): + +- [ ] Komponen di `src/app/admin/(dashboard)/desa/` +- [ ] Komponen di `src/app/admin/(dashboard)/ppid/` +- [ ] Komponen di `src/app/admin/(dashboard)/kesehatan/` +- [ ] Komponen di `src/app/admin/(dashboard)/pendidikan/` +- [ ] Komponen di `src/app/admin/(dashboard)/ekonomi/` +- [ ] Dan lain-lain... + +--- + +## 📚 Referensi + +- [Dark Mode Specification](../../../darkMode.md) - Spesifikasi lengkap dark mode +- [Mantine Theme System](https://mantine.dev/theming/theme-object/) +- [Mantine Dark Mode](https://mantine.dev/theming/dark-mode/) +- [Valtio State Management](https://github.com/pmndrs/valtio) + +--- + +## 💡 Tips + +1. **Selalu gunakan unified components** untuk konsistensi dark/light mode +2. **Edit di `themeTokens.ts`** untuk perubahan global +3. **Test dark mode** setelah perubahan style +4. **Gunakan color semantic** (`primary`, `secondary`, `muted`) bukan hex langsung +5. **Jangan hardcode shadow** di dark mode (spec: "Jangan pakai shadow hitam") +6. **Border harus terlihat** di dark mode (opacity > 20%) + +--- + +## 🆘 Troubleshooting + +### Style tidak berubah setelah edit themeTokens.ts? + +1. Clear browser cache (Cmd+Shift+R / Ctrl+Shift+R) +2. Restart dev server: `bun run dev` +3. Pastikan komponen menggunakan unified components + +### Dark mode tidak berfungsi? + +1. Cek `darkModeStore.ts` sudah diimport +2. Pastikan `useDarkMode()` hook digunakan +3. Clear localStorage: `localStorage.clear()` +4. Cek console untuk error + +### Border tidak terlihat di dark mode? + +Pastikan menggunakan `tokens.colors.border.default` atau `tokens.colors.border.soft`, bukan hardcode warna. + +### Component tidak re-render? + +1. Pastikan `'use client'` ada di file component +2. Gunakan `useSnapshot()` jika menggunakan Valtio di non-event handler +3. Cek console untuk error + +--- + +## 📐 Spesifikasi Dark Mode + +Untuk spesifikasi lengkap dark mode (layout rules, table styles, button rules, dll), lihat: +**[`darkMode.md`](../../../darkMode.md)** + +Highlights: +- ✅ Background layers berbeda (base, app, card, surface) +- ✅ Border wajib terlihat (tidak flat) +- ✅ Active state dengan accent bar (2-3px) +- ✅ Tidak pakai shadow hitam +- ✅ Hover state dengan background soft +- ✅ Text kontras terbaca + +--- + +**Last Updated:** February 20, 2026 +**Version:** 2.0.0 (Dark Mode Ready) +**Based on:** darkMode.md specification diff --git a/src/components/admin/UnifiedSurface.tsx b/src/components/admin/UnifiedSurface.tsx new file mode 100644 index 00000000..c00f911b --- /dev/null +++ b/src/components/admin/UnifiedSurface.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { Paper, Box, BoxProps, Divider, DividerProps } from '@mantine/core'; +import React from 'react'; + +/** + * Unified Surface Components + * + * Komponen container/card dengan styling konsisten + * Mendukung dark mode sesuai spesifikasi darkMode.md + * + * Usage: + * import { UnifiedCard, UnifiedDivider } from '@/components/admin/UnifiedSurface'; + * + * + * Title + * Content + * + */ + +// ============================================================================ +// Unified Card Component + * ============================================================================ + +interface UnifiedCardProps extends BoxProps { + withBorder?: boolean; + shadow?: 'none' | 'sm' | 'md' | 'lg'; + padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + hoverable?: boolean; + children: React.ReactNode; +} + +export function UnifiedCard({ + withBorder = true, + shadow = 'none', // Sesuai spec: Jangan pakai shadow hitam + padding = 'md', + hoverable = false, + children, + style, + ...props +}: UnifiedCardProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + case 'xl': + return tokens.spacing.xl; + default: + return tokens.spacing.md; + } + }; + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Card Section Components +// ============================================================================ + +interface UnifiedCardSectionProps { + children: React.ReactNode; + padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg'; + border?: 'none' | 'top' | 'bottom'; + style?: React.CSSProperties; +} + +UnifiedCard.Header = function UnifiedCardHeader({ + children, + padding = 'md', + border = 'bottom', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + + return ( + + {children} + + ); +}; + +UnifiedCard.Body = function UnifiedCardBody({ + children, + padding = 'md', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + return ( + + {children} + + ); +}; + +UnifiedCard.Footer = function UnifiedCardFooter({ + children, + padding = 'md', + border = 'top', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + + return ( + + {children} + + ); +}; + +// ============================================================================ +// Unified Divider Component +// ============================================================================ + +interface UnifiedDividerProps extends DividerProps { + variant?: 'default' | 'soft' | 'strong'; +} + +export function UnifiedDivider({ + variant = 'soft', // Default soft sesuai spec + my = 'md', + ...props +}: UnifiedDividerProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getColor = () => { + switch (variant) { + case 'default': + return tokens.colors.border.default; + case 'soft': + return tokens.colors.border.soft; + case 'strong': + return tokens.colors.border.strong; + default: + return tokens.colors.border.soft; + } + }; + + return ; +} + +export default UnifiedCard; diff --git a/src/components/admin/UnifiedTypography.tsx b/src/components/admin/UnifiedTypography.tsx new file mode 100644 index 00000000..565be609 --- /dev/null +++ b/src/components/admin/UnifiedTypography.tsx @@ -0,0 +1,268 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens, getResponsiveFz } from '@/utils/themeTokens'; +import { Text, Title, Box, BoxProps } from '@mantine/core'; +import React from 'react'; + +/** + * Unified Typography Components + * + * Komponen text dengan styling konsisten di seluruh aplikasi + * Mendukung dark mode sesuai spesifikasi darkMode.md + * + * Usage: + * import { UnifiedText, UnifiedTitle } from '@/components/admin/UnifiedTypography'; + * + * Judul Halaman + * Konten teks + */ + +// ============================================================================ +// Unified Title Component +// ============================================================================ + +interface UnifiedTitleProps { + order?: 1 | 2 | 3 | 4 | 5 | 6; + children: React.ReactNode; + align?: 'left' | 'center' | 'right'; + color?: 'primary' | 'secondary' | 'brand' | string; + mb?: string; + mt?: string; + ml?: string; + mr?: string; + mx?: string; + my?: string; + style?: React.CSSProperties; +} + +export function UnifiedTitle({ + order = 1, + children, + align = 'left', + color = 'primary', + mb, + mt, + ml, + mr, + mx, + my, + style, +}: UnifiedTitleProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + const responsiveFz = getResponsiveFz(isDark); + + const getTypography = () => { + switch (order) { + case 1: + return tokens.typography.h1; + case 2: + return tokens.typography.h2; + case 3: + return tokens.typography.h3; + case 4: + return tokens.typography.h4; + default: + return tokens.typography.body; + } + }; + + const typo = getTypography(); + + const getColor = () => { + if (color === 'primary') return tokens.colors.text.primary; + if (color === 'secondary') return tokens.colors.text.secondary; + if (color === 'brand') return tokens.colors.brand; + return color; + }; + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Text Component +// ============================================================================ + +interface UnifiedTextProps { + size?: 'small' | 'body' | 'label'; + weight?: 'normal' | 'medium' | 'bold'; + children: React.ReactNode; + align?: 'left' | 'center' | 'right'; + color?: 'primary' | 'secondary' | 'tertiary' | 'muted' | 'brand' | 'link' | string; + lineClamp?: number; + truncate?: 'start' | 'end' | 'middle' | boolean; + span?: boolean; + style?: React.CSSProperties; +} + +export function UnifiedText({ + size = 'body', + weight = 'normal', + children, + align = 'left', + color = 'primary', + lineClamp, + truncate, + span = false, + style, +}: UnifiedTextProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getTypography = () => { + switch (size) { + case 'small': + return tokens.typography.small; + case 'label': + return tokens.typography.label; + default: + return tokens.typography.body; + } + }; + + const getWeight = () => { + switch (weight) { + case 'normal': + return 400; + case 'medium': + return 500; + case 'bold': + return 700; + default: + return 400; + } + }; + + const getColor = () => { + switch (color) { + case 'primary': + return tokens.colors.text.primary; + case 'secondary': + return tokens.colors.text.secondary; + case 'tertiary': + return tokens.colors.text.tertiary; + case 'muted': + return tokens.colors.text.muted; + case 'brand': + return tokens.colors.brand; + case 'link': + return tokens.colors.text.link; + default: + return color; + } + }; + + const typo = getTypography(); + const fw = getWeight(); + const textColor = getColor(); + + if (span) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Page Header Component +// +// Header standar untuk setiap halaman admin +// Sesuai spesifikasi: Section Header dengan font weight lebih besar +// ============================================================================ + +interface UnifiedPageHeaderProps extends BoxProps { + title: string; + subtitle?: string; + action?: React.ReactNode; + showBorder?: boolean; +} + +export function UnifiedPageHeader({ + title, + subtitle, + action, + showBorder = true, + style, + ...props +}: UnifiedPageHeaderProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( + +
+
+ {title} + {subtitle && ( + + {subtitle} + + )} +
+ {action &&
{action}
} +
+
+ ); +} + +export default UnifiedText; diff --git a/src/state/darkModeStore.ts b/src/state/darkModeStore.ts new file mode 100644 index 00000000..25032410 --- /dev/null +++ b/src/state/darkModeStore.ts @@ -0,0 +1,76 @@ +/** + * Dark Mode State Management + * + * Menggunakan Valtio untuk global state + * Persist ke localStorage + * + * Usage: + * import { darkModeStore } from '@/state/darkModeStore'; + * + * // Toggle + * darkModeStore.toggle(); + * + * // Set explicitly + * darkModeStore.setDarkMode(true); + * + * // Get current state + * const isDark = darkModeStore.isDark; + */ + +import { proxy, useSnapshot } from 'valtio'; + +const STORAGE_KEY = 'darmasaba-admin-dark-mode'; + +// Initialize from localStorage or system preference +const getInitialDarkMode = (): boolean => { + if (typeof window === 'undefined') return false; + + const stored = localStorage.getItem(STORAGE_KEY); + if (stored !== null) { + return stored === 'true'; + } + + // Fallback to system preference + return window.matchMedia('(prefers-color-scheme: dark)').matches; +}; + +class DarkModeStore { + public isDark: boolean; + + constructor() { + this.isDark = getInitialDarkMode(); + } + + public toggle() { + this.isDark = !this.isDark; + this.persist(); + } + + public setDarkMode(value: boolean) { + this.isDark = value; + this.persist(); + } + + private persist() { + if (typeof window !== 'undefined') { + localStorage.setItem(STORAGE_KEY, String(this.isDark)); + } + } +} + +// Create proxy instance +const store = new DarkModeStore(); + +export const darkModeStore = proxy(store); + +// Hook untuk menggunakan dark mode state di React components +export const useDarkMode = () => { + const snapshot = useSnapshot(darkModeStore); + return { + isDark: snapshot.isDark, + toggle: () => darkModeStore.toggle(), + setDarkMode: (value: boolean) => darkModeStore.setDarkMode(value), + }; +}; + +export default darkModeStore; diff --git a/src/styles/dark-mode-table.css b/src/styles/dark-mode-table.css new file mode 100644 index 00000000..ecc08032 --- /dev/null +++ b/src/styles/dark-mode-table.css @@ -0,0 +1,31 @@ +/** + * Dark Mode Table Styles + * + * Override Mantine table hover styles untuk dark mode + * Agar teks putih tetap terlihat saat hover + */ + +/* Dark mode table hover */ +[data-mantine-color-scheme="dark"] { + /* Table hover */ + .mantine-Table-tr:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + /* Table striped hover */ + .mantine-Table-striped .mantine-Table-tr:nth-of-type(odd):hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + /* Table with column borders */ + .mantine-Table-withColumnBorders .mantine-Table-tr:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } +} + +/* Light mode table hover - default Mantine behavior */ +[data-mantine-color-scheme="light"] { + .mantine-Table-tr:hover { + background-color: rgba(0, 0, 0, 0.02) !important; + } +} diff --git a/src/utils/themeTokens.ts b/src/utils/themeTokens.ts new file mode 100644 index 00000000..75c133a0 --- /dev/null +++ b/src/utils/themeTokens.ts @@ -0,0 +1,383 @@ +/** + * Unified Theme Tokens for Admin Dashboard + * + * Berdasarkan spesifikasi: darkMode.md + * + * Semua styling constants disimpan di sini untuk konsistensi + * Edit di sini = edit di seluruh aplikasi + * + * Usage: + * import { themeTokens } from '@/utils/themeTokens'; + * + * // Light mode (default) + * const tokens = themeTokens(false); + * + * // Dark mode + * const tokens = themeTokens(true); + */ + +export type ThemeTokens = { + // Colors + colors: { + primary: string; + primaryLight: string; + primaryDark: string; + gradient: { + from: string; + to: string; + }; + // Backgrounds + bg: { + base: string; + main: string; + app: string; + surface: string; + surfaceElevated: string; + header: string; + navbar: string; + card: string; + hover: string; + tableHeader: string; + tableHover: string; + }; + // Text + text: { + primary: string; + secondary: string; + tertiary: string; + muted: string; + brand: string; + inverse: string; + link: string; + }; + // Borders + border: { + default: string; + soft: string; + strong: string; + }; + // Status + success: string; + warning: string; + error: string; + info: string; + }; + + // Typography + typography: { + h1: { + fz: string; + fw: number; + lh: number; + }; + h2: { + fz: string; + fw: number; + lh: number; + }; + h3: { + fz: string; + fw: number; + lh: number; + }; + h4: { + fz: string; + fw: number; + lh: number; + }; + body: { + fz: string; + fw: number; + lh: number; + }; + small: { + fz: string; + fw: number; + lh: number; + }; + label: { + fz: string; + fw: number; + lh: number; + }; + }; + + // Spacing + spacing: { + xs: string; + sm: string; + md: string; + lg: string; + xl: string; + }; + + // Border Radius + radius: { + sm: string; + md: string; + lg: string; + xl: string; + }; + + // Shadows + shadows: { + none: string; + sm: string; + md: string; + lg: string; + }; + + // Layout + layout: { + headerHeight: number; + navbarWidth: { + base: number; + sm: number; + lg: number; + }; + }; +}; + +export const themeTokens = (isDark: boolean = false): ThemeTokens => { + // Base colors - tetap menggunakan colors.ts sebagai base untuk light mode + const baseColors = { + 'orange': '#FCAE00', + 'blue-button': '#0A4E78', + 'blue-button-1': '#E5F2FA', + 'blue-button-2': '#B8DAEF', + 'blue-button-3': '#8AC1E3', + 'blue-button-4': '#5DA9D8', + 'blue-button-5': '#2F91CC', + 'blue-button-6': '#083F61', + 'blue-button-7': '#062F49', + 'blue-button-8': '#041F32', + 'blue-button-trans': '#628EC6', + 'white-1': '#FBFBFC', + 'white-trans-1': 'rgba(255, 255, 255, 0.5)', + 'white-trans-2': 'rgba(255, 255, 255, 0.7)', + 'white-trans-3': 'rgba(255, 255, 255, 0.9)', + 'grey-1': '#F4F5F6', + 'grey-2': '#CBCACD', + 'Bg': '#D1d9e8', + 'BG-trans': '#B1C5F2', + }; + + /** + * DARK MODE PALETTE + * Berdasarkan spesifikasi: darkMode.md + */ + const darkColors = { + // Background Layers + bgBase: '#0B1220', + bgApp: '#0F172A', + bgCard: '#162235', + bgSurface: '#1E2A3D', + + // Borders + borderDefault: '#2A3A52', + borderSoft: '#22314A', + + // Text + textPrimary: '#E5E7EB', + textSecondary: '#9CA3AF', + textMuted: '#6B7280', + textInverse: '#020617', + + // Accent & Actions + primaryAction: '#3B82F6', + primaryHover: '#2563EB', + primaryActive: '#1D4ED8', + link: '#60A5FA', + + // Status + success: '#22C55E', + warning: '#FACC15', + error: '#EF4444', + info: '#38BDF8', + + // Hover states + hoverSoft: 'rgba(255,255,255,0.03)', + hoverMedium: 'rgba(255,255,255,0.04)', + activeAccent: 'rgba(59,130,246,0.15)', + }; + + /** + * LIGHT MODE PALETTE + * Original light theme + */ + const lightColors = { + bgBase: '#f6f9fc', + bgApp: '#ffffff', + bgCard: '#ffffff', + bgSurface: '#f8fafc', + borderDefault: '#e2e8f0', + borderSoft: '#e9ecef', + textPrimary: '#1a1b1e', + textSecondary: '#495057', + textMuted: '#868e96', + textInverse: '#ffffff', + primaryAction: baseColors['blue-button'], + primaryHover: '#083F61', + primaryActive: '#062F49', + link: '#2563eb', + hoverSoft: 'rgba(25, 113, 194, 0.03)', + hoverMedium: 'rgba(25, 113, 194, 0.05)', + activeAccent: 'rgba(25, 113, 194, 0.1)', + }; + + const current = isDark ? darkColors : lightColors; + + return { + colors: { + primary: current.primaryAction, + primaryLight: isDark ? current.activeAccent : baseColors['blue-button-1'], + primaryDark: current.primaryActive, + gradient: { + from: current.primaryAction, + to: isDark ? '#60A5FA' : '#228be6', + }, + bg: { + base: current.bgBase, + main: isDark ? current.bgBase : 'linear-gradient(180deg, #fdfdfd, #f6f9fc)', + app: current.bgApp, + surface: current.bgSurface, + surfaceElevated: isDark ? '#253347' : '#ffffff', + header: isDark + ? `linear-gradient(180deg, ${current.bgApp} 0%, ${current.bgBase} 100%)` + : 'linear-gradient(90deg, #ffffff, #f9fbff)', + navbar: current.bgApp, + card: current.bgCard, + hover: current.hoverMedium, + tableHeader: current.bgSurface, + tableHover: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)', + }, + text: { + primary: current.textPrimary, + secondary: current.textSecondary, + tertiary: current.textMuted, + muted: current.textMuted, + brand: current.primaryAction, + inverse: current.textInverse, + link: current.link, + }, + border: { + default: current.borderDefault, + soft: current.borderSoft, + strong: isDark ? '#3A4A62' : '#ced4da', + }, + success: current.success, + warning: current.warning, + error: current.error, + info: current.info, + }, + + typography: { + h1: { + fz: isDark ? '2rem' : '2.25rem', + fw: 700, + lh: 1.2, + }, + h2: { + fz: isDark ? '1.75rem' : '2rem', + fw: 700, + lh: 1.25, + }, + h3: { + fz: isDark ? '1.5rem' : '1.75rem', + fw: 700, + lh: 1.3, + }, + h4: { + fz: isDark ? '1.25rem' : '1.5rem', + fw: 600, + lh: 1.35, + }, + body: { + fz: '1rem', + fw: 400, + lh: 1.5, + }, + small: { + fz: '0.875rem', + fw: 400, + lh: 1.4, + }, + label: { + fz: '0.75rem', + fw: 600, + lh: 1.4, + }, + }, + + spacing: { + xs: '0.625rem', + sm: '1rem', + md: '1.5rem', + lg: '2rem', + xl: '2.5rem', + }, + + radius: { + sm: '0.5rem', // 8px + md: '0.75rem', // 12px + lg: '1rem', // 16px + xl: '1.25rem', // 20px + }, + + shadows: { + none: 'none', + sm: isDark ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.1)', + md: isDark ? '0 4px 6px rgba(0,0,0,0.3)' : '0 4px 6px rgba(0,0,0,0.1)', + lg: isDark ? '0 10px 15px rgba(0,0,0,0.3)' : '0 10px 15px rgba(0,0,0,0.1)', + }, + + layout: { + headerHeight: 64, + navbarWidth: { + base: 260, + sm: 280, + lg: 300, + }, + }, + }; +}; + +// Export default theme instances +export const lightTheme = themeTokens(false); +export const darkTheme = themeTokens(true); + +// Helper untuk mendapatkan responsive font size +export const getResponsiveFz = (isDark: boolean = false) => ({ + base: isDark ? 'md' : 'lg', + md: isDark ? 'lg' : 'xl', +}); + +// Helper untuk mendapatkan color berdasarkan state +export const getActiveColor = (isActive: boolean, isDark: boolean = false) => + isActive ? themeTokens(isDark).colors.primary : isDark ? themeTokens(isDark).colors.text.secondary : 'gray'; + +// Helper untuk mendapatkan background hover +export const getHoverBackground = (isActive: boolean, isDark: boolean = false) => { + const tokens = themeTokens(isDark); + return isActive + ? tokens.colors.bg.hover + : tokens.colors.bg.hover; +}; + +// Helper untuk active state dengan accent bar (sidebar) +export const getActiveStateStyles = (isActive: boolean, isDark: boolean = false) => { + const tokens = themeTokens(isDark); + + if (isActive) { + return { + backgroundColor: isDark ? tokens.colors.bg.hover : 'rgba(25, 113, 194, 0.1)', + borderLeft: isDark ? `3px solid ${tokens.colors.primary}` : '3px solid #1971c2', + }; + } + + return { + '&:hover': { + backgroundColor: tokens.colors.bg.hover, + }, + }; +}; -- 2.49.1 From 92b24440fe2d6ab227bad78dedee9926dd3d4410 Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 23 Feb 2026 14:38:28 +0800 Subject: [PATCH 92/97] fix: Quality Control improvements & bug fixes - APBDes: Fix edit form original data tracking (imageId, fileId) - APBDes: Update formula consistency in state - PPID modules: Various UI improvements and bug fixes - PPID Profil: Preview and edit page improvements - PPID Dasar Hukum: Page structure improvements - PPID Visi Misi: Page structure improvements - PPID Struktur: Posisi organisasi page improvements - PPID Daftar Informasi: Edit page improvements - Auth login: Route improvements - Update dependencies (package.json, bun.lockb) - Update seed data - Update .gitignore QC Reports added: - QC-APBDES-MODULE.md - QC-PROFIL-MODULE.md - QC-SDGS-DESA.md - QC-DESA-ANTI-KORUPSI.md - QC-PRESTASI-DESA-MODULE.md - QC-PPID-PROFIL-MODULE.md - QC-STRUKTUR-PPID-MODULE.md - QC-VISI-MISI-PPID-MODULE.md - QC-DASAR-HUKUM-PPID-MODULE.md - QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md - QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md - QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md - QC-IKM-MODULE.md Co-authored-by: Qwen-Coder --- .gitignore | 3 + bun.lockb | Bin 430353 -> 439585 bytes package.json | 1 + prisma/seed.ts | 4 +- .../(dashboard)/_state/landing-page/apbdes.ts | 4 +- .../landing-page/apbdes/[id]/edit/page.tsx | 140 ++++++++++++------ .../[id]/edit/page.tsx | 12 +- .../(dashboard)/ppid/dasar-hukum/page.tsx | 5 +- .../(dashboard)/ppid/profil-ppid/page.tsx | 9 +- .../struktur-ppid/posisi-organisasi/page.tsx | 3 +- .../(dashboard)/ppid/visi-misi-ppid/page.tsx | 5 +- src/app/api/auth/login/route.ts | 32 ++-- 12 files changed, 140 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index ebd64b35..2f3afc79 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ yarn-error.log* # env .env* +# QC +QC + # vercel .vercel diff --git a/bun.lockb b/bun.lockb index aef74337942e61e4d58bfe7de080d9d40a25a913..c1f0eda6f7b56f308b202e8c48ad2eb2c05c970d 100755 GIT binary patch delta 87815 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 delta 85078 zcmeFa2Y6J~+V?$^0vX6a1}UK^1f^Jj1StVg2?(O15Ckcr5@3MP(*!(fh#nNXFyg{` zL`ATRir}&05k-#;6~%5mqS!@6v46k+T5Dz!lIOh7d%ov=ukZRU=3?bP_ix{;m%V21 zna!%t+bp}e&6S6AO9=!5R}T2)f*q%=`tz?hyz}ha8C7{jg#w4KuliyL_GZ9{k99kc}w&v?Bb zZH|4T*NeR_LL=~R_|p_EFPmLFnOe?;H^!b4TNi9qd{9~-K*<-UlZ5oD{Q`j|=nvS+ zWi)>jdk41k4X+(2qx_!r_VuXjjo!Y)+fxtkHf7%Id8L=f@>04~engGs1uZY06Ahq6 zrKLrSZf@;zf0pAa1w2|*77bkBOS91Hd$e2@$7f-O(KFCY^m1yUMtiDlAkYXcjTTKQ z?^axP9xX4wekgt&7oSR(D1+_*H9`ke1)5S;R=#L{G_WD|b4r(fw~`#?U%VLA(K#qN zEuMrbx!KX8X|to{fvXr0;>W_N!QyVQ@u}@AS20}FRHF}cJ(`6owQt~x{TnJfhAQ>! zPOcKsIrEFkXB1C~PMa2;QXUAr16LPpMOCS}on6_d{TD#h zG(Yrk1%1csR#XL8(90Ec3%0s({=Bksx-Kxaw`(Y{ z>ym7N%H_S-s`OSxTREy0`83~U{3e=({jArGXjAOlyB(7tlB&!uASl+s1>%jXpy=||nXd9#Y?vgy(CZu94rmfwq?s`VbKGR|R+ zbelRkFr%n!-h#P-!0|`Bx?FjbYxiHVRl1P_UG87|H=m@>kZ>L+aF_7$q$`V1}|L#d@x zW)zi{cb{2y%Scz+$rW>{Yaq~RR3MPU@rJ0*9G@4uTEB#{94)@z>$PY$_IXa*ES|+1 zHODEarrRJ?&DJCKR>p|RCgWVA?MZRjFUMBne2(1&eG^q99C514e*Ab>ohQBjS*N)+ zIC7$Ev_{y<YDNRtdcg2+MbBd>w&YQ-Fl09c?Md|E7WgsvSpoWjm zEiW!Fj+PabPM=>?S{B{t6JCv~1*S}M9TLM;sH?Gcyb4vvMi#nkW@0P7c|P80 zsM`6$MXp*Oql#C7!kaDbahB`p@}kL;izd&Gj*b;%c4^k`Y?sYhsA@g$?ATeEohw(I z>r!8gD%Vm}bsvfzhW0|0+i8nkq9>xtZ6K;-I(vUk__K@40)c3iiywh2y#?pFR+x)w z{!B!b-f<|cP|4<^Y~JiMqk*5!cfIfxs$us5s)8&-)l7?u=4dSpOkUy=8i&fS*9ER~ z4n#G+n|Up`(Csv%rKR&qyG0iUm%4O@6HnvcU}rMU7hlX9)uhu!F5x3^P(s}(q53Uy zv0J^*K~G&(dZkM^^ldzTFs{=@PIB!M- zU50+g5vaV}IW)Y&W%x6;T4DyvsQUA)8POu)URh-_a zvi%n^HT)ilx!H3c+88^&OS$tZ*RH#+ihbUsdu12n3bE#D=hhrs(XU?VTC1#hdhy(8 z^8#sbwaWeYskXaOo%23LRrtqIW&f79FT2*+Uwd5!S26zSkKb{fwU1c*D{qw0mtHv! z6jO!3^*+LdXd{kSpc-J)rWTj#__uK(N@?Y&wu=zjbFL=Bi<;&E2t89)Z4eAN+5Akg^{Kb?xox=ov1R5`sZpfL_#qZ*<| zKI$^~fQTBpPhyL=dd&4$bGUlyYYL!@avpc*o0(yJMlb~w8<4M<*n|;kL-{dm-5L*R#)$2+4X^u_!hwGsuQ8jc9s)lZWwne{q z%IQ{ALu4b`8eN4dzk5lqQ6+CmpK&ERc#DgWi7LUbpY>ghs%!5>JD``N8Y1)k@j`#R zFRF~%p(;Qssti9ReHH9QRL7q{RiWatZqqAg&zl?={-SH5cd410?%|i5? z^X;~`oUTE;!~f##lhAJ1?a>}+Jc9`oR5g0^Jy$TMOwsJ3snL6}RiW$9=IEKIT4XX8 z0=g{>1g6ZMH#bVfPN{s@)$CwYHOu@LQv|&WRSo}!Dxt4Ga%16DRDHPtRUc2>;WD`1 z+vlUL;WNFCLX~lUuN}QM@>=)~I;*U>4=;qVft>=hA4oz3}u2T%-J5R4d(!s0#2Hs=+cRREzmxw8m1en@^Eg`hB#4OT(ZYqqn14c&|V;cF#f8KFve+c%>5RR$dy7&Y3#5+ti}+q6{+7B$-@1wYb~N zvcThwf_AK(i7Mi+f4bSs+_H9*%b0T+c59mi1D!a2F?s+x+iMD{4mugtq&gC9iQW_r zGPD@G`$t^D2XjDUwT0I|;i}PO995&0&0ItNhN>mLMm1#O8GMVM3OGA@rX8E3nmfN? zUO(ixTC`mY$2oJCm&d|w8b`K3)c2%jJqlw7x9L)Obt{)|(Uf_mO0Y;Dqj;5OyOum1 zRe38)Z94_FlD<0nX;dBmWoxG!u{DS8^7?2Sm;VE(>a_x{+%NW8>9o<}AKSVFPl&zM zrhR1vSk+#!zpKWjsG?7eM(1;7H89H`pH@6~YB#obuN~l$-QtyNi}R)wmE&Cd8e_Dq zqP&=;BU)Bc5iMO5_&76Y&&*A7+&BoLRHt(BMF$1#F0EN7*DvRwYR?i>^=sJKwZ}C4 z4#XaU=Ai9Soeq9HFc>%(eF@cwU5kpJgC2^Wj2?m>hIT>Iy?)S9ehidHcyka3u0cDZ z<)}v7NY4*Jm0&8W1m36QI{p%>g8tCKb;wRs1^%VGYl9nb}+M(U1Sm*3Jn z^3x(Woj0n{iyY7t>4Iu*ebU1vxc5jM%5f#w2;Cq34nM^!o<4V8X_U)nWTf*-; zoXMp{Q=%-(m-ch~Ra8^yp#CnyInmPT(Vl_8K@>zMx+a;yN;_${9pRd0=>XScQ;&9w z+BAJYE}Amqqaz(}iz>sp_-T;dHP97&HC#0d7r3#Kj;+OD|3Pj}jUhc%Ux5Y@D>TEPY08r7KLYqxQz0KI{R%@@dh00 z3fePPctHEgKBTH*Z|117|9qJ9z6V?Fa1*L_x&l>hhaKrl1S5>yGD;ra2X3f$XkCsgr2r$sc>Uqw~12N_7}mo=#JyXF+Q=Ge6!Od9JB z3`bSNKT(}VhH_jrj9=`%5LKCB$KO0q=rO~Nedkq90iMmQQoaXpF*lKmo)7JkA&!?3|&m=-h*?teK$}tTe7k-=NX)v~RSl@7VdLYmXpA>vjV_OFus+ghi&_RkB z+cdmy=q{{0tj4BrUTP0Hgx>>E4;w8_(U!jz%4`q_91f$-ft1+q#~vO&7jgR1$%%D4|PPz-~rJJF{>-a`km1@(H2XeFOKsq#XJ9OyY355TE9tG6^${F81Z6z>{6eghCqPqU!J&O4Q?QPf#c`D>HMVPEZblr& z3Qz1Csg7ITCBu&J?kvDZ#v>-G9-0MKdTFtBBm0K#h&$Q5KJ~|xmulnc-lZsFM5zs!h6IkLM)09ByTP)?5mdv3S z%USI>;hCUh;w%QCJ0bmvmtv>=4sXTV&DEtnr%{`b)}6>0Eaha|l|s_ujHGKk1q9Lv zZo&8pI!vjHB%8B0r>{X^)~G3cLkqA5T1#E`0+!~W5=kaBgb%1{Ev8J3u@}Aq+#k$5 z2!s>qho1x6^ufU-ZoI$CNF}$+qZ&LoHMVQ4Yx)~Gu93_t8c0b*i@fWx1}_P-W~#z% zSnzG5$eCc9Av_WqPa=^;_;P4J5@r?z>=3XMD)OvPLe*r-1zOo5_&YQgXC;~}e56}@ z*L8AEOtWwlIud7fSVKES7M~c(0BVLb*105<*1;_(4OAn0o(PVL6`tyaKnKSdN?y z?Ts%d9G!{Xgo)e-)fB5mTdz2f3o1~xz0y|DYgX*%)4D{O925u~w|AV#Sg00~S}8@A z*XJ942dZ&pePi9O?v>TajgfeUwtAtdP*pF*>}nd!&%{y%)acFBXrXT*1rXa`wv8is zos%mGQ{68M3!oaXzBBII9m;@oS7M3o3GD%C*0{BEU>83_NyIj7?7q373}8=O?6l3e z3roEi>o77u{E;_sCsUO!n~#VpeVXQYcqEoeuQ_9j;Wigbz=zuu3z(BnW4ZKMz9S6} z=KC2OQ|mV%G>k=_%FP1CCqSCbOVi>)s3HPvXSvDpFjQk&R0+6N{S0acjVWwLs~(b! znWCX=Zkostf-0tB1?_nv=7|PXviy;MfYpw|DyLhzcKJj}a9H2Sfrq+2X~gjmp`o40 z*FrU-RSAVmOMirF6xk694U2~I4hsYZKpWcA+8iu3feL4ftOX@<4^(S_ErmUzm|sFQ z(i+C{ruU5;+|8wBwj5zv9qkk*XQ zuUP7nwAiw7eM2X3E*oYoPS~NCw^FU=J6J>QG3|SDSnFh^#@4-*pXm*?rd!DLndNS% ziY=OKy3iM3P3JW2Il>1q6Xm2%Oi7%XLzBUBO0ge^9>bc56|`1s>OI0*tY?uKScl0| zx!C-dU6`8*WXM<;Nu(O71}Byr>#GKdFnI^!%s!D_${AQ%x;X69)95fzbysq9*ahJo zusU4M4GC_tH>E;KqM=*(pS(Ym0ioK|S&P-pSVT+|`9M1WKY+&juvfefLoGRTDu)!i zwH=G4@nJ_}BEM^(!{y`hqwO;q4RqZT8+l@W=u`|6uW=3wUjvFKl<2XHLkrx*SFCUq zhRUqG5-y=fpvud2MMKiiC`g_i!zacO(R{Cud^D5+wTamtd<%;LxJ1IO2fJQTMv1jH z<2b02)|oGK1BPvWyEs>4#k(l66$`b-LAA6)CNc+08K#h^P6}=>yT;Szko^cbw&lNq zQZ2jKx3K4@4_h+xha|^Pqj-Nr&V|q+qITLHNCag zHHcRt+=4kixei?bJ*EzQ1bWKeG!i+^t$((dq}|dK9>D1Ys*(ZhwS`=}D*gtg$L#VY ze^;`jIjb?P7q_s)?;yi#q4snKO^=s1@*PNB+W_hsRCDrVPN-T~Epd}P_m@y6RFex= za*C(>43s{yG`Y-#8#9zO=NegOc2wa(kOJ?R<`UIfc0O1gj9;=_LXSZwK!dSW@8oBl z=mxc1G&1ZKBs>L+HbolnQP$pETkBEG=_lZ1N!>=*at<^t3HPUTK+?AliPD&=| z2FS<@LYY97I+d82@k&STh7MG0P78)ISVh&W8Fp`RHBa1X|ijwzMor@injuq|F~d>=U8SgE#l!9us3s})qnkR2F9*&<9O)!Ot9=;?K6 z#>xH^P%DM0P}>1a^rpGqM(}k!qd!lcl^vA3)t{QfKe* zSU1rc+G`mjy=AW{Mq*f+7D`s5&VC||yaH7bR8MW2Ti6=48|Ow-qxK+PSutH^0`~T< zs;0fF61fhlu8ud9quZeIIhc;G+l@CH@3wRrL~RN7WEq)ws=Llun?3<$zhMVl(svJ3 zWvJyFik;>LL8^vvq%~uQ1;>`?U~;h^Q^J#>j0#Kn@XTdbK22u=Sr;s9Z9joDIwLvG zedEus4rM~)J)9=L(3KF4k2E`TKEz_G(XA>#_epN2ttL@H+2{N%Hxo!-*{o_zpm)Ix zwY_j8wt4yt1S$c6)a2Us z7?}BKQwZDXdgv5)?rnmja&z07%b|nuOShHYie-DqTKi9RpYfa66X4Z3EH03IWoC)J z0Cqb_qpf!Sp-;g*g_RP^)23^{dU8gq0`7zEa;VB4)b=^@G!}!= zRxmkgb7r_J*NT;-XG56~d--mwmv%3%&-cQ5>4){welr6B=K5Z_U0E;vv|f7DtU!R} zYp)byP|d5_b^D}V+HbZSXSIDVu9v<8<=nSdx?ShgL&Fs{sX1qMuvZr3h&r*S&2{%3 zYUjThs&U#tqk`bps>bt@DKv)KbuBa$s{Rew-o6Ek1!=D`{sA2dRa$POF?S=!%&)Kg zuY=Yd13y6PraPjfz7%eQ)@`TXpjwsFe4X4%+o!ai6vCH5*(BQX9+X@Zs=;bPRU?Tb z?aJKBuGZR*kYaKg1amW?S_?FX-0sTUHzF9y0&BiE1V;$&Xu%F?yar%)AGNhJC% zboc`Q;UlqO%qWc7N6w#Haf~BE03;iilJE_QMEY-*!$H%rw_ztMfAgZH1 z!$f*5O7273K-4-N>StI;P}!T$^DCK9d(8(-NbGd=0f}9TZ@L)LVH3}>`ZwZYtY_4 zkweaQdvcv+l%5+NE1~jL1F4j*XWoX!PaQhq>X?0w8wj=Xj*N!Z9d^s1N;fgUOv!Xc zF7!yK=6I7tP47C_b&~Dccp1Vem2PJNaOYp$gM}SIe3djU_SoNg2`~&SNJ%94J9Mz8 zHc{Ida~HcJDZ#`i%S?<%>JeM~UMLe1KYu2Q8XA1Q-wq|` z%NmddS&#%<671!&V9VI12kp&@(Aj_WGg`Ne!W*%M;GKeJR$@sC{SLPMnn*W1Yl*wU zrF0v`Uc9we20&%B8?*2mSXye*Ih<^X!VBD|UnTF(@9s(;_;_DNcMJS?$T1KNnf((z zG3CNQV4A1xIAWVD{2Vl1L%X>MXD)Tc#U->@n%(dke%sH&xQ?M6M!rw#H zZ;8Tc!x+nYG%YmF)3&Zy+O}WjdbOr+C<{2yyQ)`Q8{}Q?sIrf5<+zT2DOjtMuq~PW z*q8y;4B_lPvv0WF6|VHS#6O2E{v?zI)fiMP&6(zQ4ZIbq&bM`k+J!&-F4V4p5ceU! z$(3%p0_?}FQ*7)LpsKs5(zUZKyacM&6HScW(92MpznufiPYq=)OBN_C5%~mYd^9Fc zdgnuxkus65Yoo_%Qs2l-ZJ#b~EX-OiUfB@JTAn=ZrEtJ4LhGPfZ{(WTZAW%MNBGQa z8`~wXSIoDu_0@%sd5{7hyFK)1`GvMYCqf(A%MW>1xk_pz*|xBkFEXK;-6~1)4ylLk zLrUrjH`?uI;EWMixkQM~ACe!g#ORBmG;`w{VOS^mf~Ll^3jGagyFFHTV}1rkAGz31Q%$dNZDNy- zjKtF9RGAJS*bnL9<ss4cBe9al?!r>4_F6>7uin#M zZS8ZNYZ@1A4wj2nwIx5xAF?YdWpP`aM%TNtxU2Oev2+4+i_;ZYYV-zpC&$ba(D*z` zG)d%lsItP<`Zl#=q5lo8q1;W*a;$iIcC?w?L0qi$KFT3+CJMg~jrVc#1l!?8mx9)h zvwv^6Q;-AoCY9*l0FHLW)FSD`G z+L_7smU?}^fNB!dZiarV>q}t?v~H9A6I!>z``uPwdB;L)`xeg3%>vdf?ibLZbqbcZ zroP7c3$%8!s_;Lcb<=ISwm#pn&|~(_Eqn*GH#Cjb>6bYFhF`eTeZOum-0d#+QG73a z9{AL{iM$6MUl;eeyG|myFb%CIjZeV!q;d2;?$dv=Aa0KVKCNz!-+<4ki%+_DZ*OjI zW&)?w^=)$B-VNf;1mJqSU#;V9FGC%^Ztn~&z7jmWc9z=WLZh{*?gvb~zuvk$QU|+l z%REqTjtjthwd#kPLz&QenyJTwb(~d@rQor3%l83Tt7HlTs>W61w(FA{;t+@2@;w%+ zsp(F(tFgvl>BQ13akgstkQ=uR?fvEwtWy-DR`Q{3-YIByO$z46cD-+R_mQz1+Z_v zEc7dM5Y(0X*hgK(RB`t;m-8V<>Nv!e`$fnRwMe(elGk;d&xH`}o0G9#g$$`h`aPap zV{7#K8nEi8s0wbOe+#OujtZs84?skMBLe%j5qq|Zu-rdpDGQ-=)QoV-fvYI+^y>kQv;c`-i|Ljzec-3)P8D#3lh8sAL`?(00Sz2t^KZF<)5{0kaijguwI-|7Zc?G!G7*7f}mdScx&yz165DjQHLt!=|bp<0OI z)UFiao1g_yHJTmNq3PI29mbXZk*ORBG z4E)>$gUB{_H%Iv-X%SRsr`q&hs7jEaX4A>s`Gsmro1nHBCfKz$^d@u|)P29I+uJxO0vwo-}9d>DMq`* zQXY8~tSJ?*pzHY_@4Hc=zH}EMK71*Z0UV5LN^IRp_HtC@4#+4USq*nU?3F=X%zZ<- z1@J*~71f5reXU^yV6Z_8x>YAc{pZ2JG!`cMp2Q-j0#9Z)-%w zIn9388kiWc7H+*YH?t7vfp>^9){+cn^hC*kkxg_*N|hW zVlr0m@AoEWL$omtlX7-^o<$yn9^o6OIY->O`3-bP&GaI@eyC9{+YI{B1W;!ZyLcr= zXy{3>(##aH3sCr1kTS0|F(XIscF*uo{7i!T)=~Q(-M%*hX-IHIIu{y0b0pXO&=&Ae z6~X>^ugOnt%CsO_4W|*{o;YQgqLYI88CWW{{UUkjX{=nV^jHqRFZu!ND6CXd(39`Q z?{S{#B_UMHLw>2f5SQ)i*NUg?;Gm(v)krcD|{x_NRE+EeAmuS zJoF4WA8fxo8vX;TuXk#cXwGo&U;NpMxQP!jkws8j5~y0$oe$PS)f}2ni3Kq9D|8q% zEmpNPKl4|YXBIB*t7rqOLK(oL@vyf8!+*ybEvsfKktV+-ua_Kbk8m0YErP1WQj(p$ z0W=VVk1sHv)3bmDK((bxXD^b5M*r@HN~*qgAGyL?jTDjKK8LpY!>@y}BED%p4$F49 zPsi3f<3?zIQnz2H-Hve-hWn_h1trw-&v+@T8_};=@@`=Bh<|dt+|wG-wt&>7?Q93X zM!|<;*-O{K_GVM7p#A)uF)`>4#GV|UA6ko{0c8h8csrKuVO1&Fr&%dMJ4MtIiSGl3 zWeR!mD~)VhPx}l4OsCKB!|Q{ zXfJ%+R|`XF&c}VVumEcY$L!rL)1hrJEnVeks9VM1-#Iuy7P;Av%b|YhK|8wam4{Fj z)+mm<%wNMg5=%Xr_UDS1d<%3QR4t@e8@p5N%X2@}LK^Wp zVn;~i1E?09#v%l}TMf1`RR_>P+!V^l2-=TC?&jT7SSn#!Ea%Pq3?3jd2*Z9gEHnp8 zJ?z@yVJsEQ_M6tRP!^;gM44#8a@ilkGevAHh?>Q19+yJ<;2X4G&fki4l49DgiMQuh z*8_bzs(eaWMn z2HnREvo4K6k7cWkcPKFuBf%_JFcl-QjzmU4k0T+GWJOm(RZ*bzwV8%LhN@9SBk>bU zs0YtIQBSeTGHWZa3Tni3tJOnL<)WA^l0|CHBT+Qq)XJ){8xW^MHEC7QWY@2O9t#yw zus@~=HQ3J$(tz$xWembP1|R#a`3o@A5otKvHOuV=UiUuGfL#C_&gQ8rn#_CQiC|3v zi{n)euk_TGE>WG(>tLe_auojHN-MO4(qy)+?dPRb@=q)*Fy8 zLyurDyA8gUV|D>jVu_-Mz6AFq55C(K4(6Mx++eHN{8PJzNA8~-mG&2@Rq5GjZz^9jEUR%fVC z$=~3I+IDtLZ2QM;cBg=fYZbxv<_q9xU7Xh?d7?{}EqoKWqONy~uE9WAUAz<=KU?}l zbZ-Q@sIK$;gM*2$K-I|cQ*cQwZ}a2{+_^nuZ)|Jx9C)uBb!|ESP}fOnr5b790-jzg zZJS7=!-Dquz+T=9!0PF}@JHZ!vMlUYr+`;Z4P`>-)J>xYPfOB_pm@w$Ev*Y6>TWv$ zBM)O0U};wClagJqf{je|0Jql*jq2eixc$I!CDtjpYr#nTt}nbK*RS}sXecjeFTvLK zy#qR-P6}x~gZ9xGwS6Z-wd~cVH$$~suT6i14uoo4Czi5MpTpe}z%6)wpFST;>A8;| z4`J1FL2@^t4*GpQMK7_5z(aPs8*DRd090aT}Ww*_d-eD zk0OBBZnbrO7fan4v_D}VJT%$m$+Cq$2iro#s&371c&zJ#89259ZpimU`D+EyKJC%|t|71)ifVaK@zr*_oPjbM#!-*-Bz zd<05-B1rU9sQ>ZaQ#+8zxma4iY;t7j7J!}5_(-uc&7R})PpHx8i3G#vf#X9pIol$f z2JIpePuYF#DSf!lPxn~Dr($UY%O|mdg;ped^jq#Puv~v(W%NDKjZyoRt{BERMb}Tm zB41#g98Zh{?RUdNN1fy|bOWHnt)UFaAo)3>D;*z!2G&9uV@J3quop+xV7LLG-wl3) zHN1GrRoQ)@8&>EiA;f)fxf^18&Tf5w z!m<-a%T}bvC|CM4yp@JM6@*HlZX)Z})n+Ue*F9|`e6p*Yt;vZPDiE7*&SfzyI}T$x z$L42X*fearm<~nE)zQBAwy?V9!}ldKfOeGH&xwzCOQ~ms$GD7bR%0+uw~d+?3g$P& zQeW9ajZcXWXyDT@T%F`WlUOKKA^@5xYu;=Xh$zPhvzy?ght( zuZ>{mQL}M=g>?PS4@|Qlnl$oJ67FPsD^%lIR|z!u-AekAk8KBD?syl`o-N$|wH%}m z^y-)79ximZ_YCMtX=E2xe73oQRsEEkHNka)PJ4njzwr6cLef(SIwU?nO*qvJJ^3Zh zNudX!nnr0AMbUYSYyd0rk&&&lc4%Uq#g+`$I#lP(^_(W5PAlz7P8l4 z!i`RIpBhvqd!5>T-VcojYk=An)8-#p2AZsRc*@&-g#8|>4@k+G7wLMsOH!xmP6XSp z!6VJqsmv#xfzAlJuWsq<%b~BabY(2XlthDlBZp4%r6Xdp|K>sKoCzZ@L*r+2`MT3= z+akY$_!PLEUYMIz1FG=WHS|xY3h%zY8kp?bi^G8of?KM4L38o3&#wrbjiq+6*B?R; z`WSW@5ADQKO@gXPb92Lt;DM1&Q`{KePw5ft@(`L1)npFo3>Dglr7H$C+WHIcpfRC~ zd2YjZ_*8e9NFC~~T=9`E3#h3Jv!CDH0`xny0IH#Duh@r=iYCub{`)dIb3!#n+-1LK zu?8lr$h7!M!Xgzq5o?fAw9DQ#ShlXD&oFgu6?vmx+HtxcOZX)wLu3Lpe!44L!uV{x=*0LG#|6V0*16ffa2^i1 zRWM+Vyn+kjQ?CeSGtmQq8}x@)ecA}VhM-|_J3%jL6T$|9s0B3`+0&!&?^Kr-)jWDYO3;n-BPD2 z;~U=oPgL=@^VDIR;X4=t?-4XUs|k8ZRiKXvD!>kcj(XY zKT4*hKmPAj#mn}7Qgz_|s1TQ(thSfIORBdW7=|a51VfdA7&fy5{lSA!CE8h@|Ane> zTm#XAS_4OVzyCmO{3y)e7vO?J#DWXJ1miWg};GWYFaw zV1s5~Qq4I|!uIu_s0z;M)4rrCFy}A(lIrawZ%<;AYsVc+v3*GufrZb${++6Zv+W+g+Jl# z|3Hr?`hPD0sY1W|<7854GfE*hMMy{0qajoUXskb86)_WAeog)H zh(BIaweYs`ygpSr+5UKT3YTv55>UczQ5B%QcaW;a2ce4C$sa%1AD41X~@a zBR%*d%@am@dOh50FO+|QKHl!@?S9_wFEy7f2zDvZz#oRG(w*QB4@dQ?sX9KwbE&c& zDK-0F6Ks{E!>9PeQXL+P3XRtv(`}(YYZdv!lTqb93ssxUL-`jd;g6!1dM!h>(?1K9 z-?^w>Qe|7^`4Z2i^1BEXx`aP!<0~~96d~phtk6+jQe}9xw`-~rx)!c@H~8aHC3rKc z9rss83 z%cW}aw%7N(V@*{7-iIrLfBEAzRW<(9^O~x3Klfa!biVMmRBy+8<$+WVUwgZz3Vq8T z_4!Z!xKsuD&D&DF{X>7uWoHFjX(|L#R0yw1J{4Q20e=*|u|FYpT#F{>cC2)G7S6Z(M{i98kousA@FPJD!H>RZ}&@r+ELUKVGkz zIX5u%mER2SR#Ua`pXIq!Z_nXR8`OCIKcH@}_CJsi#rjVQ=J$#!fIc$b?`!sdP3;!r z|5C>4>IbNx>i7_UG@>f?|2XT--$nOQ4YeoWiuk1Wd&>Jso5J-!J!|dRhRW{^e_Sg2 zO;qW>4++w1JnF&q!U0dsZIyoz5V}2m0@qviJ(J$dUe$g!ny~D^=~8ouaB#S z`ft?rlImmaXm2Ofe!Qsfz}}Ak##TmS{Bfz?j`y}y$0wlb(ut_XRuQTbW94k#==Hy& zN>KlSyk7OGj?d+|x}d`Q{TEtUuY(delZYzmUr>#=bA3YpP8BcSqEbaUpFcV1t=?WM z$jd4HuQ$rzPE<{FH>y#(-aAUgH=rueqyG5Qs50E_k3Wklo#(y%g128r<@XAz7I_oR zME`{{{{5T(Vb!VVGcu5V<1_qK8S;{~w7VPei_dxZMD&FC!P=30tzkQ?Q0twaS+Y_*r@hH#hQx$Lw$ED-E zUrkj3^#6DHm-B=A?>G59f%o~JP&G!8Pfx1hGX+%{r=cptbX05I{Pe&~ z#J>4osuCz8qFz#cc3kRhsg7UbZK*Q4)Z0=nS>DFQ(t~ab<_1&DoZEt}#>-`ecaf^B z231F0=ebmGZ}N6cRs2u{U`o8z8sVOFW zL9kUw$y6&jbK)AdP^#|^?=98gPf!(Nr|0#lj(_g`zVz|F^8Qk#^R++zt>;qlUEW43 zZ4dt9fm9B^db_5opnv$|fBNH6wOXo%%U-H<(ma={zzw{eR5NpJu$i60A%9G&xQ$Td z8TNJ)f4rv3uc_x!y=~#`f2S%-EANN84SO~~3AXmy#yiwh9d8TQo}iOIF4aZPeyEQ3 zM^*46y?qp_37@cNESK^TN@v!SFtv7#K~>>1Q1$i{ zxuH{0y`-w(G|#7do#D0EAD@LPy;78afra|xRSotRY^8g4gTNsB<{S_7A3tftosTM` zC0;K?^^z*$Qg2JSmUxH1mUyYhQn_8`^>VLQ_~Z3y7XE+ZxXO1k+6Uc^svaMpO7BBd zFR6<65vq3jqyhKXbnHw1$nh&w34QDBT{_51stCJL)$+N3eLirbXfIo`29#zH%XH)@HOu*)nfH2 zD*lD%^{EQ>r9UoJ3w(pZD+AxUH-TL!{{nmT$E!9zjVY#T8b&o3+n`Ede^eRgpqgBH zsJfyrs;(K}?LjF20>|r*SH(Zk+VsDBQ(INylW*2U=XVxb8sfB~=Ex1Fe@-mlc-D;8jx#_6GmE z_P#sO|2uc2H7@quiDqzWeD22|joW{B2U^2X$JKEA?nJ9KYTlXFI=Ammw8j#zGE}?E zeRrbWwAyzk+763)??~^v6RkVyAy3ciS4@+t=%av z-I3mRCt5A>fBcU0zB|$T?nI~g%h>zwMDM#3z3)!6E@tXF=e|49G{UL6;_)Aib9&a}37`|d>32>b3tvpDElz`i@tt{pzqV#!OY%Od;kL~~bC%f-Gs(fjU1 z|Nrw&w7zu~zXN^RhN^pm!%sHnq^6u;vQtySW*hH+k;k7ldD!Zbkd zbif`{l@92d4%i{^i^*#M*esb7}Fe&gUqVtfMLx6=`8@AO<@bb9)a}&T}`khU~LOPv?bsWvrb@K zOF)ZOfWu5tD?mmoz-EE&CXx-qbV_>~=oquAJz!XSJkmShG1L@x0PGQ1FEGpma{y~Q z0HQg7VN5E!*lT745z(#?J0|6t=CV?3T0y-T87-dQh0%RWq z*d{RAz}iCr(Zc{U%{qZ`hXGo21I#u>-2fTg0GkEonn-uRMuCd%fca*Vz>MyIPCWpn zrlbcTy9Z#KK)K1u1#A&mk_$M~Y!z6T3+SB(SZJ#90A2F{I|R-$c|8H!1y=L~oNcNF zmi7b;IUI1VS#~%e|8T%=fyJhv7htEr>Ry2J%r1eIy#Ql+1O94O^#%;<4M^_;xWE+l z0qhZ2FR;`E`vTVX0Yv)(E;j1~#`OiX$Ol|%it+&&`GCy=mzzjGz(#?Jet;{@CV?6K z0G;{+mYb6Pfb9N&Z30)BoFf2R1eP2DxY}$LSa<}W_W;03Q#An4bpT+8z_ljtNWgZ1 z6-NTDH`M}5j|2=k3UH%Yb`&80D8O!kn@qvcfSm%Xj|SXqb_uLJ8Zc%c;8wG0AYj-) zKzae-Hd9yt*dwrBV66!b0<0|nLfh9u$51Fk33x@)F9}C!Ms*VM8Jr=M- z;O{1H7+|}=ieZ39O|`(%VSpjW0UkHYjsxT$2iPs}q$xNauv1|5@qnkyE`gQD1IC;H zc-pKw0WjBdNGp2AjV2{9hfh{I@B4F)sK=eew^JbmExDx>_P6E8Z6U_k`CjmAK zyksIH02>7=MgU$hn*?Tz0CXA&c-54Q1Z0l{Y!i6h$$%lF0q>h-qXGG&0lNi0GzDV-I|Wvc0eob3 z39K9g7;_5XW3%cMz_3#Q>0<$(@F*(49)a}&J56vLVC`5ybR6Jwvknma!i2^nUz#Gx zSLR{K*CsLn`NkAWzBQX9yG-j-k?%~285!r1PNq#h2B|n*iPeb;YD#_30 zEy*t??{o^V{WJ=&;&ck|o2do_e>eTkNEw^*N6;)kLpzF~`5e`bViHH$QAkqwPqz?u z6h%myStVgdA!%R=CnM}ABpD_+1z|@aX=K((*ilSHGEI>rY#x?0F_9?J)D%my%qB_1 zw4R1EGbNJ!%yW|FCTBX*!Yq=sG+Pm~a5@#~J%fs5o2nV`*5)lq8y} z{-)nd0k~}_Y!m2Za!LVP1eTNn`k1W(3rhjL%K-VNstnMz46s9>zsV~HY!_Hj4j5pn1(ucr zhExEKGRrCe`4xcO0s~FKnSh-FtIq@sGP?v;o~aGP0=HpU02sCakMxCj3^j!d0eb}2 z3k);CMS!&n0ntT(GSY046q?p&Bcn`- z3aXGIbB$!O*(I4`jyn&TYF0_2=4Z(?Q+U2AdY&qJKGm3Ef`5elBHyFrr9d6 za4DeoMSz8->LNhbivT+W&N6uy6Z0?TBFWjNT5^u*cL{Q?SthA8pGg**f=iJqbB*LY zvkNgRFD0Qdmyyt4&8o`)!!84)Ukuo}H=1Qv1M;s1>=wAm6d1rx zfz<|Zv)LuE(g4P+1l(#?tpp5P2}r*NaGNQ-2Czq9y}()%yp}rNZpKUQFzY0Dn$UH~ zU8YEKw|Q7{kBMB5+-r&@_nA$Sb*A+V$o-~7@_>0x@}SAN5m|2*Nggs=5wq|{vh4jg zvfOB@{s!p!H^2^ozni?90NVvt+yr>kR0}M<2{2?8;Bm8T6(D~VV7I`Nrr>74PJz`o z1D-Ow1XkV*7;_8YX|w7Uz_42Y>9+!&F@?7R_6V#O*kXdK0c&psL{|fzH|qq(tp>EX z4e){~x($$V8(_1*OD3`guu-654d4~CNnpkrK&Q2US53)UK=xX|Hi6eo&h3CL0!wZO zykWKqEW91i`wqZ%Q*{TR>m7g{0&knVI|17TR@@1A*HjBEy%R9xF2MU{*@Zors(03VxG_W*|714zFY@QEqB7qCZQy}(WrybrMUUO@Cd zz~^S2z_|MWE!F|PG)3zG8S4O>1->?s`vDsTD((k-Yc>hYxF68z0l;^r=IbH5isUqK$=Geyq=GM)u&7U*swTL2pcDz*S} z%_e~vTL7J&1N1Z{&jGTZ18fuMWpbVeY!O)UJfM%+DzNZ*K<|G7@=eu00bTzI*dfs0 zUIOeCSp5=Ukl7`$@+H8SmjTC^ zRWAdEy$ncy1u)bUz5>`IuwG!832p_feFYHR3OL@Z6BxG@(Bf6Va8vXuAmdfQW`UDT zE?FH3=@18Vap(yY1T>DGQ5Y(HboM)43fDf@;<_rK{DTLlCWj? z04X&k61EJIa+C8RQehTJ&NN#Qv+zR-*Sng+Ei_fts%tf1hrn4T?<3V!V8us(vrV_35@#`&|)XxQd6`Okg*f6S>SRL`3$g8pyD&Ym1dK` zjL!g_J_jr}C7%PbKL>0RxXR>w0oWq2jjT?E!2TSg{B2sHqlMx(6`iXTalT+0TIdp8>lCo-_r&0Cozj{sr)q*(I>@ z7r>Zb0Z*G%zXFE+3P}GA@Qf+^4X{UGy}%X|{2j3NH$e1v!1HFEz_{N5E&c$!V2b_# zWc&fxEbx+v{0Z17Q1K_=6|+fT#-AzS6`fMqTdXKa9lIhsHN0XQ*6SuGh`A+|rU+uc zVYXtMg+V~?6u@>+FR0@xw&w#iEcY!_IO3V7F43oK0q3`qmLZ42RAtJ47=nOy=a(*a`|06sRW8UThh0Hikrd}0b40`>^37uacn8GyA70nrS==VqP2 zxC}sx5a3Hw6ar*~0GkEAHjzevjRF;o0N+E--ZvQuDn79hp!5?Gl97!v`cnN<uEdZTb0-BkUmVoS*fNcWJ zO-?Jo7J((L04>c{frYIAy|V$?rYalIl@Voj2(&SItpVEwRF@|>f0%yCR1<>@c$@n$mx~(NW+xt&)@L=5vk8pwo7>agb8*|{YxzCn6#9eF5A%k zsMOMw)}O7gKRLCzpIKZUEu+AlujW@qX49b59_h^uzhO&eJiyjVeiWAfNb9jdsYj%L zd7}zPwHw>!FquPAGt#eH%hLut962J5E1ao|(_8b@-9{?J$x0_Yzo>LdbT&Ux=Ha_8 zzwlh{L96_u|ISZuH{jXOHl@j(O`116j-OiPFKOYQc$jW09Gkq(Z@68(l+EmuQm2L= z<>!a|7wZe}`Yn!G=hyLSG@z_>N_TTY|Fk?a z@Tj!3WBEempYEl@zi?{S*Xs1#2EF38R$V=4Uaw!h4-(Mx9`yRnZ0eu3U*+%qkiPV* zCqC-+hiCeN+5wgX0)Kj@Cr2FTS%3m6Ydu?GxMxAn^sK2~H^$L5Y276q&@oM6kp2~5Ls?MvaXDxU?3#Jyz@~kEA^_+FJM#M9A4S^R4 zdNtEyJNlmo98p6R(0J3MRYSv%Np-&z_^N>2|e z{VVVF%J!^M{i`O{tF?#id9N{`mj&Tpl;c@vn0?qj8h?^w7v7JhWz<4E)zKEPEARCrIMwnX z&kp9j9zm#=9`>k;972dcMX+-k!&dN61xl?Nb-_`EJ&d5oOsP-@`^epR&tA*E4)Lt3 znuFKthnhc5O6#~`z)@*W1{>(HUEigfKaNg2pi)mI8ABLFIGJz?VKhNcInfhPE+Iud z>VFvTk0A^p3?}HYDnkj!5;Sk#B5WgUCp=HsOn8m(BH%4iAu!-<8;Z?#@ge`>U2>&3wOL&3s3gH>T1BCktnwt+2G#@t-GzZraH1F0E^jx@d z6D&wOAw8eB{rILs+JTjWd3y|D2;o@5FoGV=_9Bg}+4($S3*iOAX2NrXmk9qPyh3=H z@GRkJ!r25p&GbRS0K$=kL4>0S0||WzM-%c1{Rur%`48KA@unxCH{o!CrhfsUE1@%? z3!xL?V8Z@{0|*@mn*9w48H9}tgNIYhQG?Q2R!-%;9+({^Oe0Ju=sDEa5%lY3J#+hH z!l(3OHDL$gbHYx-$Am8k9}zww=yB^G5WXUON%)ZPFT&S^&j{}m3P@`(VGv=U9-RFu z@L9r}ge`>E2rm&{C%jD9MtFhnBH=B<^MqFj+X-6<|0Fy|c!Tg1VH4pA!bZY+!lQ%@ zgufH?MEgew4-+0IJf#0=;W6Im5%^m7ZYJDBxQ=i=;ReFhgo_9l5|$F!wg+YqiU~6b z#}SSvLZ> z34I8M6AmF9M(9R3l+b~|XP7`oLVLph)7e>oM|FG;JGvE)#obA8cgR3+*Wy~--KDq{DDG1Hd(Ra%q@=(9^YQRz?!9y7jGfUd>kId3qz_;b z+=08my36ed90lo==fEtO4Rc{0%!i19{OJp$VK@wh5x}z6Z48WpVK5HHLNAcPB@zZf zf9MTEU@-KA0niWnKzHZ?tdaB&6WQr`Y#?9l#ZXY?2&v-uq`ShQBf3O}MgM8vh zK95ujOdrsOky5AU&jobdU!8LB3{n7S6$W_!BOI3}-js zCftJCAj8=N!c2roFj+=`D3}6MVLHr!cL;ufPhcn42_PXXAva558T<}YAsQyYSQrEH zdCuV=pUP|o^`Jh;=R9TXtO!-0Dnyj$Pi2@u(Ts)RFapNGL>LL9Ue2!z8ME%3L4B#JeWuo;0 z_Z>U~8N{E%Yj_L6_~!+gipbn39~nBs^#L5qc>4#(TS2}V-2@syeW)$-q-q@e1~Lnf z@0(YGB#;>71Ng_`1e}7?a2C$Nc{m7rVKj__ksv=S&;`0dcW47`VJ^&rw2%;zK@$0J zbY}R6b}YYAQGw&txGO<^E@VC|fQ2v?MuYrn$zW&)?V$s-1^HPRerZkrotjLznSrT@ z+go7U#qA9|f~W8po=LCYU(S{-sFdb&VOsE5`p&k4Rt>8ELU8|PR7Lvm| ztaTkMf(3AgqPq(++sgu(As5IbEhosNE*r>1PCoVA1A0O)klEmM!j{1O5eh&_C(AwN`xvQP=CKvgIY6`&eagmO?C%D_%y*#w(GzTDpo`a(aozMR%H zVit$dFcp@3=3c(EP)lU5>~-#mZ9<9~=>|5db2oa_O1VBNOCPS^sMN%b?Teh_vh9~T_~@)_4VP!#(s z26H){2lHVOEQTep0#?Er_ye}UR@eqRU?=RB*0l%r!9h3z$KW`efKzZ9&cInX2j}5W zC{N^(FdSOJZ_pa%klZ}2O$gyzr`nn8Q$ z03D$dbcQbQn(BE4Z{P*I1UE{}2G=;g0XN|mOoRy(eKPQX6gJ8~DF^Hj>pzz^m|P75 z`4FpoCASdBw@~MhtGQ4S%E1e+y@Gcrd=DQ$ey-#$+<~F^jRyI$t`G9^-PAYm7UWk% zW0{#N>`AqrrrhEomR+jR^2em2YDY&v& z^n{cGN^FFc#o|c#6#|fNf~42p)oXqh9C{P9>@?Xb9I@w%luvD?5UwLhyi(n zr+QvlOP3}whlxPiz4QUn)@78H5%;CaUParIz}(IHvhh(B?Y2#mQ+2kQw#hge_p{bT zm8qvCQ6AN`Z2p$8xS7x~59We(UDc|tRWan(RoCu)Rj%gO(DJ1gDH-420k>21G58!45ZNi<8TOMQ8P91D_3UsblIRy?9GhvDD@`eoBCi0cPw!5@BvY)}C5 zgDhEOi6TputdIt>fJ{SVI+78bAWN6@VCjjelQ1oGk94MhD6s@2fSgO@f^{tZax967 zsZ<(`C6~waleJU;@O$gOuuCY*t-L1RQ!lE+Af{?7Nus(Ch%0%QGLYq1Ok$Q_%t)zmR%sv*~n;|?GbP+1L2FJ*-h*^0=>O1Zgty@lzDj3ATOHqt)DBBh&a z1roV<{$^gc&P68XvCMX@bLrw@`df+U%Jt6B5v*&SVjOqjxV8ugl1oXoB`C$+(>#}f zKem5guJwV^Buv6c0;E(dxfkkGZ7o%Vb!w@#&aWXV1thci5ikS>!yp(413X4ohJPEQe(# za&09@1R`hYk?@;g9o$C$THIT>8*tadCfEpnc=Ct;`o@RYs|ajSJ?pZMw~xcUum^U7 zgqMEs1jjFMkK-PLgCGTU0QV3a2C=-OxJMuZBhmxhcF5evmFST$E|5tz+tEXB|(0WP$v83 zARJ`|+4l9&?J`6H<*4N_zx7&BstNc=^Kl!%oe2`r^$<-V-x zF9`})yBccF&`KOu1ThuKgQ%(rl1?csqX3vO5M^`Do55Y1O4&#YNLY(w*^h3a$~Mw+ zhPUFdC5WodxOyzOT|v}Hu}R8AZBLLKNOY3Ql4^A$Eo<}M9QFcn9boV z5QCcu(_tD!g96K9r*bSBr$EdYBtvpt>_w`{stW76RK*Oi&SQq9%p>#&#L%q3k_}7P zsz-@HWW<$96IcAi6&s0tO(GFJiy^iyDaN&2UjwV{D!Pf5I$|luOTaP+(JKZaaYzAJ znG!>fsn1HYgi~e}SpHUcG5U=ldSdFAI0UXme6@bLAT1(hDOrUOGw^?9Q2a!Xgp)8< z%d#RA|8EP1<}RrekC>6{i*YTc5-UObO@AxLa$Q>EA&4peH|MuOR<5_;86_o4RQXW?jrFSmZ4o z9$ZTVvYwYaZ`LlAwZEK5HlSsrSr+Z$X?a*~3a(8@sTb}KAPOZ1qEy!Dnnq-}vZW?l zVk1B{5{AQ2=m~W|G9s(%A3^Ly5-Rr>Ln8Q78)|{)RviYVUA^aFB<~cLGDFnfb;_XX>sLVq!UtuFGzy?a3dsDqSzlKRT8;ugJg!R zkOgF0Lu7J+>`;g+I}dq*yP|G^AUU&g70R*PAeAYs+%OfLa9Dv18TxAAi5-xGGFPz zakqFpHPMxWdQcanSmd9N+V!heI-)6;ntwz%vFXaObco$K9|Dme zwK5o_B61gVlf|Rbv z%JBePsYxrW$XmK3fiWW$87D|Wd&g_-O6@Q%OePT%K<-IME=EE$OoE9p21dbn7!6}V zu8o5UAT=0;I~f#AfvNBoo~e$l*bsQZ;d9tQ6pwHpfLPCcSPnBlqMir)U?$vyzhM!` z^|^2t?!aw0fUf;;2zJ3=T-yVCIX(=xIKBzotBuh2ybp;09+HTY9N&OBa2>9}@30xJ z!U{M5Tj3z`XL0w#6}Su+;R5^#l7aJZ0*=EmI0{GLFdU3mn(>)~B`^zO=4dwOl3G&f zwi_ft`*0-};z};%cs|z`s$IWPm=(Qo~mH<*n68SoaS$kH^ShcYpe^DkiB#L+8?t~q% z4I~j#*jqpnzX7DI{s5_zO|TK>O4lw*MVYW2qyR;!l`AXNyG@yx{*p-X7c<)nl3OYC zg&-v>wH&jOtc;5FNM_eX@aG7DL{V%{Q7kzZL8~;xK4V52^IRg8gowS%bv*&NVxFQ$ z5_ifx785)NfoJ%08m!2qG{jS`$G#>?&MI45%tlJqN{0m51J*T3gNRy|c!_gKn_QDZ zl5|NJDI76*OOJ$;bnh|!Bzi$)tuiprBlL`l$;3>fl)RXMWt$S=byLarj-^wu3RVhD zPat7#m|?_?=`V?U$hBu;HcvQ^2K?9*l7v_}h#BZ9*QMlR=F+Mm$(b}J=@epzk^Ae< z@U!DeTyJ0)*f<|5X5+?zD3%`Anh{7B_yr_UpWzt11L+Pw!AJN2@6F?wk&5zK5Idn# zDw1=_h$Kj!SB#Lit6DB#A-LRMnB&9LWB?8HTbm}`nl-}f;C>BGMmbiywi9jAp zxPZG5M32ad5sSY>EP9PZQ~#n=io|u%xMmfVJenyRJ@VLQ9^_UR4(l2(%5G~A77!YcmqAyh@1gnfDqukz=St)fA~$ww>6R~? zOY$yFkBEv)|J*+5(TPw2{zF5*nBiuXcECN;hnQRi06aGY|J+86P`t(uSb%3RG$ zk6KS+BXu2tkhTc$Fl^-QR+W47-n=~kfzW_J(HDtNGMt>}7wl^|;_vtP1mY18;x^KPuIFVgLl6UW&zKDC3^ z8@s$d54zCuwr0y45S$OCZX;A>1VUya;E6#06ZWh>5487n39K=F%6GSwtNQYahs$Tb znjyO7p(8JXc34<=(}3O&XSoEP@xc(%;I1BuoL>TNW+C@}nu?X-yZ{*WjS0^Vcw?36i zV2PT80Cl}ZoSUj{B(?iFfqK?Y^KtrNA8AGJ)@9euy<2bBY=Hqm0r@b|A5~HWd2cy9 z0`=XI^GB}v)qvE6){QRYl2fW90=A86WDLi+_#dGRwY(HAk@&@(3NxK%vaL**S$e9L zL~8wcM>=PJlGqw^N=gzJ4S(D`OJhDlWmtP4lYZ4u1co3WhIQDM;!@>h_jJPw2o6AC zlA73G%b9U50@)DAgat~w>FrbSeXF6>TX@(kABVcxUpv8D!m9_6=jrMQj&mtyBYJc1 zSfA$CoRPsq9E`o9ca!>pK*#|Ec#=GF!Kz=+46)xWW~PjGc;586*XQo}3~G5wJ*eIa zc(-XFx?dq6rghnKz`48g?mlzruCDqcVDneg#F@_bVYG$`w_zjFN)Mak_Q;Mq0owu`3EE!BFERxxBQtyctZw#j^9LAjoX?Y2Pw-2Nol z?J8#GfzR8>&t<9{ZSv8nfnlqz3-0=NEwS4?ldz!T~z>g6K zy`z5qW&V_vQ+J>@#3;rBD)A6q@3z5oTA9>y=>^P-161ZATJR5b(-_IUw|GSI^XJ_= z*lp!i>ml?4{%SP-1ll!3E1B+nI-`94UBkMM-hZf*YqbPMEZ$mJK;B8J{!qPtTlUrQ zRcrcaDd}i357R1!pH6QS$iJ0ZJ`*z-7P4m0@%}3+evEIo(bpA_j(Cn*IgI2zRpp0E zpPNB_9!8(*p&~eSR?ld32L)CXFYaLy7&K z)~M%3s_SSir?YQP1}+r+JKufPmeDBatImzq3fktW6k|ARu5#iyyXQ8lpvc1eAA{1i z6Z4@BN|)1LHAEmJ4gm_wH^At_JC)n~B-e%9%UmhXp?)8um2`H?V+<<)PEj|NbS&0z zP~{n``IbI~oHP^r!e@tDwA_@_ZVMp^p&0jNe3IaEq-NBy(`N@Xv@=ZL$1w5A^a-BO zwAZc~v-{c^D94IUH$F>5_B_=Q9A^&7QM&Ly2Y9becqW%dM;Rg-g4}*mUl0hXjezvz z7eBn+pXl*lgIxk0@sTleWkkfilnI|sarul;6~__9eD&)%t)g=~aw(CkyDCAg=B);` z@stYGJB^d-1ahhVo&ARd3Dm9IVH_S?t@*fP-0WVc{NuHpd|sjHcrCrN4mp*Q={&s5 z=XDD*^>Gz(Gc{{Gma5Mjywtt%ns>$}l$azWc6^>T%V KY_`ESAIk5^h}+aBp%k# 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 { // Always run seedAssets to handle new images without duplication - console.log("📂 Checking for new assets to seed..."); - await seedAssets(); + // console.log("📂 Checking for new assets to seed..."); + // await seedAssets(); // // =========== FILE STORAGE =========== diff --git a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts index df9cde1d..9b5cb438 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts @@ -38,11 +38,9 @@ function normalizeItem(item: Partial>): z.infer const anggaran = item.anggaran ?? 0; const realisasi = item.realisasi ?? 0; - - // ✅ Formula yang benar - const selisih = anggaran - realisasi; // positif = sisa anggaran, negatif = over budget + const selisih = realisasi - anggaran; // positif = sisa anggaran, negatif = over budget const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran return { diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx index e22b2617..2c84dda3 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx @@ -53,7 +53,7 @@ function EditAPBDes() { const params = useParams(); const [isSubmitting, setIsSubmitting] = useState(false); - + // Check if form is valid const isFormValid = () => { return ( @@ -76,33 +76,62 @@ function EditAPBDes() { tipe: 'pendapatan', }); - // Type for the API response - interface APBDesResponse { - id: string; - image?: { - link: string; - id: string; - }; - file?: { - link: string; - id: string; - }; - // Add other properties as needed - } + // Simpan data original untuk reset form + const [originalData, setOriginalData] = useState({ + tahun: 0, + imageId: '', + fileId: '', + imageUrl: '', + fileUrl: '', + }); // Load data saat pertama kali useEffect(() => { const id = params?.id as string; - if (id) { - apbdesState.edit.load(id).then((response) => { - const data = response as unknown as APBDesResponse; - if (data) { - // ✅ Ambil link langsung dari response - setPreviewImage(data.image?.link || null); - setPreviewDoc(data.file?.link || null); - } - }); - } + if (!id) return; + + const loadData = async () => { + try { + const data = await apbdesState.edit.load(id); + + if (!data) return; + + // Set preview dari data lama + setPreviewImage(data.image?.link || null); + setPreviewDoc(data.file?.link || null); + + // Simpan data original untuk reset + setOriginalData({ + tahun: data.tahun || new Date().getFullYear(), + imageId: data.imageId || '', + fileId: data.fileId || '', + imageUrl: data.image?.link || '', + fileUrl: data.file?.link || '', + }); + + // Set form dengan data lama (termasuk imageId dan fileId) + apbdesState.edit.form = { + tahun: data.tahun || new Date().getFullYear(), + imageId: data.imageId || '', + fileId: data.fileId || '', + items: (data.items || []).map((item: any) => ({ + kode: item.kode, + uraian: item.uraian, + anggaran: item.anggaran, + realisasi: item.realisasi, + selisih: item.selisih, + persentase: item.persentase, + level: item.level, + tipe: item.tipe || 'pendapatan', + })), + }; + } catch (error) { + console.error('Error loading APBDes:', error); + toast.error('Gagal memuat data APBDes'); + } + }; + + loadData(); }, [params?.id]); const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => { @@ -162,23 +191,38 @@ function EditAPBDes() { try { setIsSubmitting(true); - // Upload file baru jika ada + // Upload file baru jika ada perubahan if (imageFile) { + // Hapus file lama dari form jika ada file baru const res = await ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name, }); const imageId = res.data?.data?.id; - if (imageId) apbdesState.edit.form.imageId = imageId; + if (imageId) { + apbdesState.edit.form.imageId = imageId; + } } if (docFile) { + // Hapus file lama dari form jika ada file baru const res = await ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name, }); const fileId = res.data?.data?.id; - if (fileId) apbdesState.edit.form.fileId = fileId; + if (fileId) { + apbdesState.edit.form.fileId = fileId; + } + } + + // Jika tidak ada file baru, gunakan ID lama (sudah ada di form) + // Pastikan imageId dan fileId tetap ada + if (!apbdesState.edit.form.imageId) { + return toast.warn('Gambar wajib diunggah'); + } + if (!apbdesState.edit.form.fileId) { + return toast.warn('Dokumen wajib diunggah'); } const success = await apbdesState.edit.update(); @@ -194,21 +238,33 @@ function EditAPBDes() { }; const handleReset = () => { - const id = params?.id as string; - if (id) { - apbdesState.edit.load(id); - setImageFile(null); - setDocFile(null); - setNewItem({ - kode: '', - uraian: '', - anggaran: 0, - realisasi: 0, - level: 1, - tipe: 'pendapatan', - }); - toast.info('Form dikembalikan ke data awal'); - } + // Reset ke data original (tahun, imageId, fileId) + apbdesState.edit.form = { + tahun: originalData.tahun, + imageId: originalData.imageId, + fileId: originalData.fileId, + items: [...apbdesState.edit.form.items], // keep existing items + }; + + // Reset preview ke data original + setPreviewImage(originalData.imageUrl || null); + setPreviewDoc(originalData.fileUrl || null); + + // Reset file uploads + setImageFile(null); + setDocFile(null); + + // Reset new item form + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); + + toast.info('Form dikembalikan ke data awal'); }; return ( diff --git a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/[id]/edit/page.tsx index 207fef4d..4954d05d 100644 --- a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/[id]/edit/page.tsx @@ -82,17 +82,17 @@ function EditDaftarInformasiPublik() { await daftarInformasi.edit.update(); router.push('/admin/ppid/daftar-informasi-publik'); } catch (error) { - console.error('Error updating berita:', error); - toast.error('Terjadi kesalahan saat memperbarui berita'); + console.error('Error updating daftar informasi:', error); + toast.error('Terjadi kesalahan saat memperbarui daftar informasi'); } }; return ( - + - + Edit Daftar Informasi Publik diff --git a/src/app/admin/(dashboard)/ppid/dasar-hukum/page.tsx b/src/app/admin/(dashboard)/ppid/dasar-hukum/page.tsx index 95aa7979..2dfbf3b4 100644 --- a/src/app/admin/(dashboard)/ppid/dasar-hukum/page.tsx +++ b/src/app/admin/(dashboard)/ppid/dasar-hukum/page.tsx @@ -6,6 +6,7 @@ import { IconEdit } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum'; +import DOMPurify from 'dompurify'; function Page() { const router = useRouter(); @@ -68,7 +69,7 @@ function Page() { lh={{ base: 1.15, md: 1.1 }} fw="bold" c={colors['blue-button']} - dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.judul) }} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }} /> @@ -77,7 +78,7 @@ function Page() { @@ -129,7 +130,7 @@ function Page() { c={colors['blue-button']} lh={1.5} style={{ wordBreak: "break-word", whiteSpace: "normal" }} - dangerouslySetInnerHTML={{ __html: item.riwayat }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.riwayat) }} /> @@ -145,7 +146,7 @@ function Page() { c={colors['blue-button']} lh={1.5} style={{ wordBreak: "break-word", whiteSpace: "normal" }} - dangerouslySetInnerHTML={{ __html: item.pengalaman }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.pengalaman) }} /> @@ -161,7 +162,7 @@ function Page() { c={colors['blue-button']} lh={1.5} style={{ wordBreak: "break-word", whiteSpace: "normal" }} - dangerouslySetInnerHTML={{ __html: item.unggulan }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.unggulan) }} /> diff --git a/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/page.tsx index 09f64047..35ec8174 100644 --- a/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/page.tsx +++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/page.tsx @@ -9,6 +9,7 @@ import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID'; +import DOMPurify from 'dompurify'; function PosisiOrganisasiPPID() { const [search, setSearch] = useState(""); @@ -100,7 +101,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) { {item.nama} - + {item.hierarki || '-'} diff --git a/src/app/admin/(dashboard)/ppid/visi-misi-ppid/page.tsx b/src/app/admin/(dashboard)/ppid/visi-misi-ppid/page.tsx index 6b3791c9..e118b3ac 100644 --- a/src/app/admin/(dashboard)/ppid/visi-misi-ppid/page.tsx +++ b/src/app/admin/(dashboard)/ppid/visi-misi-ppid/page.tsx @@ -6,6 +6,7 @@ import { IconEdit } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID'; +import DOMPurify from 'dompurify' function VisiMisiPPIDList() { const router = useRouter(); @@ -96,7 +97,7 @@ function VisiMisiPPIDList() { Date: Mon, 23 Feb 2026 15:11:00 +0800 Subject: [PATCH 93/97] fix(profil-module): QC improvements based on QC-PROFIL-MODULE.md - Fix fetch method inconsistency (convert to ApiFetch) - programInovasi: findUnique, delete, update methods - mediaSosial: findUnique, delete, update methods - Add loading state to findUnique operations - Fix iconUrl validation (make optional instead of required) - Add DOMPurify for HTML sanitization (XSS protection) - program-inovasi page.tsx (list & detail) - Remove console.log in production (use dev-only logging) - Install dompurify and @types/dompurify Security: Prevent XSS attacks by sanitizing HTML content Consistency: Use ApiFetch for all API operations UX: Proper loading states for better user feedback Co-authored-by: Qwen-Coder --- bun.lockb | Bin 439585 -> 439994 bytes package.json | 1 + .../_state/landing-page/profile.ts | 264 ++++++++---------- .../profil/program-inovasi/[id]/page.tsx | 3 +- .../profil/program-inovasi/page.tsx | 5 +- 5 files changed, 121 insertions(+), 152 deletions(-) diff --git a/bun.lockb b/bun.lockb index c1f0eda6f7b56f308b202e8c48ad2eb2c05c970d..7f208dd45ae5352b8d6f6932cbc0a55425cae64d 100755 GIT binary patch delta 87607 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 delta 86746 zcmeFad3;T0zxTh^N_J!!6LUfkiWq_zV+;~I34$0C1d(hKGLQjLnrN$N8^x+#XjQe< zqNNlyloq9?rf4bAD%w)CijJbR&*!tQYv<%Xk8{87bDsPBe*f4n$>;lhU-P)Gwf4fU z8~&@W`!8zM>=8e0T1e$HHQW9A+E<59>|OPKQPN|xe6_1*GWU5-*fC|_;;mj9KaCfU zsBd)cvUo;N;4p`#RnoMA+}zY;24zprqG(yJ#H1W(^~EP^ z4?i4Q587C1Ii=UVH7x-AQ)pf2yGoZq{oxltYeQ3%#zJes?+UF6Z7g+UK@|l`%E^H5 zpfwQTxYGBb)#1OX)KdC5)EE2?epP|yWT&N$N2{iTSAd^p?bB=J2YYH7YFtnmSx{G2 z)3nOa`|w#x0)8>|Pw=T1lr}>h8SjMh*Ffp-Q~nm^C$|eu%1q15693`nq^7&Fz_U{l zr&uEl-~1oY4@?WjFb$fRm6e$HmY=NC352t5;I71Mm-d{h;~b?|(4RgC&wihl=-x4C7aR^$|;utUKM)FLsm7zH3Jpbv<+?Sxu0KPNuyvUlsz;a%Ei!EX*(zv zLjaT`XhPXn_gl+~ey8*tlocpwFDrTyJ_mA2W_At+PfPD0`!)&6co~TqnW@S7nkEfR zOV7+n&CJlWeW)o1A*hoqpdOS7u7Fe1F)xFY$7^Y+*>+3LfwRYMh02Oug!;fgp>!X# z3jBAKzNT~;v;xX6m^3|`J)$-2Dr-JzdZO*W4WHY?awz9ex{Rhx%FIbjMT|+ASOEBZ zsw$A1k(8A;B`0%acQs`*Gbg7a#za?6@RZD~oE?bA9x2-p>l3ev`9s2Fiu(KQA$NW*S&_9~nLg$^xdiGVF}UMOZ7!)ysduDch0k z8lO86(U&7zPL*Ska&G6q=W==+$|>ba%gathU%wkI=jt*jNB$`&vwx+peQjV*1w+|;m%MFOm=^4G>UPtPomW%g1&!NY2q=NWi(8 zl#-a0(`r(7c&u#M_*_hId=3uQv_OQv3FQ^*X(+Ey8BlC^1%s7#f!2oKP-z7y3;rGX zapU<0${s!rt;V@L-kyS!viqU}tXY`S#N_ngnW#AJKGv{vB02oF%|17+76P!7?? zIH`9G*^svJvZt=YXZRT?yZ*~jvLPi1&u16yRSEpZ$gV#>T8`EF1X<%7@LBLHV`avF zDSr*(vn7R4w%Abm1Jbd@r=i?_K2*9?PyuOBX1Fs+MhHN|S+FmZ z72OEV__;}~(o>VNGQF@;>8B^>W~E_>?jatB(3O#snv?3vPRyD(B{3`86{qs;17#0* zV5YO@+9ja>S(7RVpcbi?CuOG{eS3uPBZDh+|M>*^?VC_RdyV$HwDoU9McNu4q!50iCerX1rV z;H>c5P*!*)vAdplm=Il;QcYDxfQr1zK5h(lk)K5|mw*gC)g+|JG$ezd$)8 z-$7ZxA;lL$8LvaGoCEJdIV5+WEcjI@clw4<oW$|t6UV2yenoul8>dx;ci9!i`k$PZ znURRGUI)MgyJyLY_|KLtsRHFBdlBKRU_O*B&QS5kDc%8di4|-BWyPyP8UM;-n${9} z3d)MzFOcQjfchX#!9^JCvX2pg32 zD=RBAE7&z-^?aGpk4VQk&}o6x=1?}|Ae8xjiE!pqgbH%(H$N?VF5ns2pi0kR{d3GF zWxKMmw6!w`;Idf!temYgq3qJV;M6t9fCUy6%5V*w1%BC0W;{?0i8`}s^`DdBZ$jCF z%c1Ov6kJm{CuXMPyAqR8u&)aE&k4eiAuHHgX+tOrtPW*HUP^yQ9*kGAM5_Cu^$mPh z{4|v5zC}C^)u`v?J}}4+Mg;)((XiiAITVYRT2-sG%0DDz#Ur7NwiU|s9hb`<&Q6_} znlT|$dmWtR4~DXXmO`sQ=RsM|F;JGDr~Kd-rN2;V2srbftit=gWc&FAFT!9(&nm^m zF%@mlx~T+Bp`2qipd9iE$*EZkuc!R%oaA6$wx^}$V6WD+3E8e`*qyW8CrnrN_}tXA zWbN2$IhS`p*}&JK)p>PZq5@_>{SlCC2WSNYpxm#zLYbfmltWw{S{156t3$89BIA7l zrT-zc7IdBBFF=|8aTPuV%Jie@b9Fjlu%LEOMyv;ALFE|%`s*qgei2IlQz$dY%1lek zouX;|-2lOqU651dV|H16^z>> z`F1EPymhA>%7su4*}f0tMR-znW=03(-#-6CIjfsOS;Os!zzqGN^w%OIu8w$QP!W0o z%9hXFC&$zUpYdAlmjxBTXGOA<{LFR7BZp$_DXai^~DCdli3cro?3_k-!Ir#;)%o!AIjyG3uQ}Fq3rTC=tAy%b3T`2*Z>8y%d08%Qks^I+fRH}oR&TI zB$O4%gw}wLf-?U|rR}Ks%m@a%$^oqm{o|BWcgxeWamGxVoYlWrR%G=#dAZM38VPNM zbaj;f6XFHK{}9?5>dyZz8o~yR{#sTHOC~WtEiu_O5P&V}3S|vzLfJ**@hm5JhNdOq z{KCEU_noq3N1$xk+uv$h6KMbMWDCQf%x58ZJ!lq`LpcJe!D3*s0wcXlH?bmH4uJG>3d37D}7e!^n7I` zLD}^Km3CDc1mzA`Ln)U)Vp_K5K5Ke_b7Of0L&Ed{zsXhMgwNICe#(%ZJskt6X?eI| z$%a=Dz^S@W-HRNVct5|L&Gf0$`U-%1+Kq zM@tfQ-R|1|I55cqbF$iZWRFbA&B#m2Om^+nbbHnhMtm033(5-RW+bO3p({JWw+l?1 z;bITXNKfMffNcA|(F*pdkY5uq8X$#3rL-Kny#xJ*oY^H8p`1-$K+$IfMNrlhmpz{3 zT|LX`S`hpeP{wNrWq69WZqMFmDEHjfP=9Cul#^zwX@}=Cqu`t@mn%IvBN$IP64#;t zdjm*KP7TJhk1-W=yYFj4neat~bD_VC=FoTH$#q7KrbSiJwZ`z9L+e2+Dt)!GO#chi z5Bxl|CbTECBKP1uzB1z@0Gu`3l&-2OTV%jzi$bf(K7I+xo>&Owpt=iKgm|n_nrm7H z>ZbjSV>IJkRa$^>_UP^!lH;15lM~=d)+W31rZGWEO*vS|)Q)(&rZv;QNq)!Lvi!uP z%q(V{ILVH!RrQlS`WM1k@7yfAzqF|+hjSqr%8q*0UuqnDuCo40N7s?{7y@PcI)HPo zG*?<*YK4Lq>dFj%sB1N^Q!l>;INQ~so@`G`DAOmqTvPD;T&tkMC!}U12jh&GU0>## zq7=`MGn4Q*>pwq7c4g=0q+)wb(o) zlnZDgE8+`f1K)(QL%a~)2zvPunbC((UNT;QHiKqBId!8H4}i9S|4SoXYYzPc+7!A* zX+E?GXUr%Vk078kv>~)QlvD2(ZlK7ILRrvjP-gHLl;Igr&Vc7-OA6*gS>Y9}C7%tS zBan=iH-OfHa>~vPk@*I-MR~kxmxIBsNk;(J%Aq!Lkv#=vhW~lWUIAx@>!G|BErBv! z>cou9ESIK5qaa>-+>c4ysDi(&DC7OnQ4aCYPP&%Q7Q6<78)5{M{-015{8Xr%Diad3 zbEaahX1Q{+QzztU?m5*3@i=$fH&o-Z5|g+wwd^8!7L;r10F-kI-vv!{wbir(s0gox z8!PJh_Ug^;Cc7r6yX-R0o^s=xz+3plq?E^dNd7*;Szu)-Gwk0>R=f|w*#jHkbFQpG zICq6Tz2&0%4fn=eL{EmxIaVV=rk~JloD9H`Fv~U3HRD5M#Ecz?z@GRO3Ah9Q042|Z za!kKPhTI({IAuX6q4Z})%5+~rS<%lTt?PB`<$r-}>AHY9$DfXt(Fee1NA!fUqgq2* z+$VjdUm58bp3_gZs}|bD@Snh0jl)o;`vlctyaf2H<~HTOCaalWumA@4>#0z7)MzLx z(g(_f6OfR*%d~-Vq5!BpzQc=gQcH2JtKIs zD?cy0L=~_O$_UG$%(xKBsg@08MqQ!osm4&o`x`ALzYb-EPb)nLW%{R~oa|8az7}$7?)gpyP4&xp!{DE zpFQ^#loeiqInDfbkCO4DM$3kULG1@u>8Tm`qAfeeeZ!XRN}uM+(zJ~#!HZB9n4FoE zla-p`dJH~ScMg;V%tHYTe|fAdcrKLNiTg>~B>1doB$V+I5@k=c9;?@|yQUTb*hSA{ zB{M^-ph|=Ms!1~UCjr@?BcV(^Fj)?8r1D!qSz1Sz9ET=Q#;*%yW?8OeJSx$&Q4?gd z--MD6fN~sg4Y%XBo+!C`w&In&1)z$xA+S+{r}9qy*`eUWV|9M&AAO>6y<=a^oI0!3 z$yb^!`u^r47}g^@qYPFG>P4xKj2d4h4;tw7-Q@{ zb<_KTb+|*QwWv+)pgV;%XH-8jy=!uV(VM3{_5Fb%!VX*9W^a)IP|kcGD*lPrcH1(6G;cd}L;(PrP-t zeS_MwVt&kcppqZ_Yn@u=n9VTX0L%^<61$VKZ!m%ZL6U%7B< z<9qejZ&}nePTz7dY@F`ndbV2ECC9dmU-*R9{*wREkFB1))>%`#*SC6hukG_{d~Wm_71v>4_0CaiGB%#A?^Vva+O18S8qt@BS6cGOx80ho z4e{x{Xmy3H0|V+TPs@FJSTkqviI09N->v(ccl1T;ws#CX@ZGO7Bdj*PqrAQP9a{DF zxtot#o7x1`p8x(EpVgjzvgKDDqc*O8>(R!2vWC`wBd+V?*0SDFR#1}$*2Ug~^nF%H zc%*UL)0!FX)B~-3;gOE<4o&NA1q=>1x4{a*ysu!zCWe_;;9&(77*=dtm@(ALDj(rA z=6PAW`Z#rmr8^__&Q`QD()O{oTE#HVRGa3shsnmP>AG|8q{uA9{`V?z(WTbxF z{@u#z6%}a~VpC0Uhnv^ojdFW>AM0dPq~jGV%Q!2>8Sc0PYbY(fzjZqrA=pL++aW*0 z8f1s`w&wJW)R)-5zp;O}wO03w)C>6A@hP@~{&qZnE2Mv`19J;&-58|m1B%SU(X{J?PY z7OXIo;9-T7umHcok$Se3I2f5=2k*{I99Ln5a^&;|R_7s+dYm<9NTj(C`(-FndRig3 z!pstQ5w^!M?T+oRqn#@}piUH!z0ZCT*i9ZxnCL>W}b#O)b@Obhv@^XsUsqdWp%AxBb?@E zxQFd+2kEh4dUY#0E>cgo7RN;zZw6TB;+&4lxNP*W(szd&jp|t|BAjMCcHbUI;o$Vs zg|#_8(l}Aix*PA*y{uj%BlV8f0{qUfPL7N;KfwK6A4INT%?S_FD_M!7BF#|^WRVW* z_VzG+lXV;H3YhFwvc}e&(UE$-b!2p;`2!BK>^0p=ObF8(S@8*x`b2AWLZo9S&evg9 zrLp1W%Q(vCAx=3610@}+io&(dx(i0<%f+kkqM5nn6=XkC|53Wjw-jpMN zIjBclf#V|`Prz?$-546~*aoYkwIM&;{DBryXjWi!Ppel_q#kcANW#p(twuX=!`eJ8 z%q)V}*7n#JK2~CKq(0JGmmFy>Y9>Q8%Xey+vA>zs#pN`A29zDdY1O>BJ4&x1Vftvx zcS5B35?C8M!^KHq<|pvv_My+~>8+w0_V~24LMBEUueGpdPIQ`ox4^1Fs%qBlIc)j9 ziIL{Cma+jD=pkX|c6gi#I+ud+TT5$2ic@cET}okxH%pClTnWbaC06$#;d-RCGc^*E zMV}ODUP9^pk<8Ny92|zBSUf3GUu6|figYx_CKqR&pA_zxU22_%HQef+5#fnG8EOaS zms+308f>%1=)Xa>l?$ssEn{Ch>+uYy;oaUkn&EUrU|;BFRhk;^*a54D*c{+d&1y5a zjpKK?9qj}yJ6P8Hy)b@dXNiY(DXEhY)zKe0+so}=Cj@F8)PT%&OWKP&I?N5MpJEoIk8fXtjb8Lmxja!CstCO`W%V{(XwTx`1 z8HWMwi84*=_RuiXvoqReci8PUVdhYHQMSjW{gU!L?3mxdbJ}6(T1Rti8%}HX9}(U( zY>YKzz6X!P=84r3X8sC~)4?7MBe<)*=qF>~TZxs)dW1Ku(FoQ#SVqHc*7|8qb1M1+ zV~_bd=Z!F9WjCwKbfU2yUV#@O_Vm&td|;vX3uNzT zS~+XZ=1^}`j*+p?V`m%$4^>3{u!9(H^{|>h>h!$|h^{NpRl@ex)KQ(x`q)93n_+LT z)NruM`(e1ET;mLIDtoB&^!%Jg64Ud(vHzV@^ zJmzWljp>6#GEbH;ywqbSzXVUFLob@&z(XM_UBhtMZ#vf$uT8URS$p%_INkt{urF%Y zVaeXZj%zlIkhL}J5t$ZY9W8Jgnw%XhFV&HMyyf_z3OL-1^W*i@6?J9Jd6=L0G?JLA; zf+w3AI6us>8eS{w{K9bK%3$laxUXmLDOTW$#Hw)ClV+U_M|g z#KiS^O_;G^gf-G~I&J}m^8n;%7st!_{rGTWN}Sd9d8e^E&YB6iA7`z{0C$Z?Q>_j2 z!+mpM*|R|ng2S4c+{t)1-YUP;>9{uv=VEKmstC`~JVaJn8tzyIs{<{w-55C+?7sKi z36DeT-gTN;b5@1wW362;IDLnW)ig}A(l9@vm(xSNK{-B#z13cUC+F|U4Pj=RaizOD z#l3Z!tDI)r6j=+Mlg5z@FTozveXvH^ zR^3!hi?=O)qa>|QORc7p+^nF~Duy+}PSIwv+nQHueGO}v9ojBU(}vjAQ>9ihtXP{h zPj_37!iurk;ZmzchMSFth1(TOM6XP|%}L8}Wm+TGIDOv&;C^VRnOwozH>i{23TT3T zo>}rn8%OvQ)llQ*Db~z4oR04RWbM07b+_$#Sh6>6!jhrREL8*Jr7WxYTBqX+06WMC z&bD>|%*kfW_N)ze?1p6@rOgspoF=%?V{>hrW1U;)bUc=W!MDz@3wP{-)rFRC|6H}} ztGeU(Mm0344bhcarhP_y4!sCSi zhn-bnjverDxZMV7;=@Q}4}wH*y}c&b{k2`{%naBOz;X9D>KKOkVPSfX8a; z*5+hvHwE~+9=69r{z`b^wug>1D$lW=-s&_5&5^r-X6?ht|G7Dq|2C&_c#d6Awa4Wp z4@-SWm?IKiSNr1j_~TZW_nf|gPsjmRJ)+s0v6%oWhlGPC;K_xJ?9FfCaq#W?EZ@i{ zWe2+lqo#cTfA2}_`gW&TcdmOj;o@t=&9!FkaGEOt*;Dp;(x~v1Req<_?Dmu_(9_=C zroromDE9fsaU0%vZj`>?Tco^^D()7N7@-sWO{stI*D9n4fv4m>6m#_DZ& zEYm)6nAhR;geOl3;S1cCcW$gt!0Rp3;S&BSJoba#R@3`w+4uGn1Eb&5)}-A|$6mmm zR?+luqwO=+-Q7;#$DWZnxa)(P4D6pJpqx{7PDYDot)-V1P_ z>WGEc-s-+2+?)$*pq(eX;gn_dJ>WE|JZ~*I;52iem-jv8c$RXUgx7;NvPPAq)}(_@ zbJ$Y%guxg(UW3=)K9k&p)kbc`!`c}2U$CD3*l7-bL9>tJxNTn=X1)Ng4NCR4un>(C zFIejjIUQq{;rL)}I23NKhsB1;L_Po``%!Atz(d?_U;~jXt znB2Iv+`4-PSHTtTn~|vp!yGfLtG~^@ge8*(zK%&~ zv&pbz#_zz2vI4$|@Pr`~gs*XD@(ip2c7kG9GPLmUDK~c=xwsjAd%yQHgx3vqFEU?yAcXg(~lJSn)Y|r-*p4)gp zLCH%lcsjVI>~ps9{x<7+g46s1p#uQzC2aJ0&pLP4X}u$|u1`+fKFz)Em9;o;uqez;=+thQF+$Z+!%ELmZ0gSB_dsV^^E zgWz$m@U(ApUxfD`1%JQWT5;3qXtak*mszI6>S|j>uwrbh=?B<$L`*$B!V^{>GP4*K zyUpIV&Bzbk8#V4DeDmP7Lzw&G>&f${`8H@*Qu~hNdwA>(-R{#yd*vm}!`k;km}3IG zFuUY!usYjT@ILIqw2W2zL|+fx>G%ai)--%Sw?)=|AuQRBW2Kh&NA5ft9l#eDcDyHH zjk2u^u;Ohi^q}f|$8)9D6Y9DgGM1I`_gL`ox|TRE%my7sXUPwiH!nYR%uwBugb<; z0B=fZSP8tGQZN0yyRLjjfBL+*TN6E<^|kvM0-p{X|Jv#@#A%efAfB$JJB_{<#Annu zF9`qIh^$?dPrE$qZznpyYm4mdyAa1jcyh_S2TN`$pM8tJ9&0bC#P8Jd^?e%_2TfBS zAKnN#Zh|I=*>$k}T;_zk;V&(@Q~F$SKOo_a#5j01cWj3x7i{_O-BtpuSi8)PrPkl2 z)}X8I(ABVHeQ(hcHyWYDYi_m>7LHNH;l>}=t@XuD$Iu_RhX6P>z#3~?6@TQ_jVu#Z zs%@Qxm1tX?Z*c!%=<~2-ygy*sX^r7O+1YOSiEUw|@;BXO$Cp}bVc9+D>wOEMWqPeL zPxg)xpt7f5FSTyNk{e~W+iq)Ksr6Z@74Wk=G=-K3@Iw1xMcaLMuhi=Ei<>Qk6)Dm^ z5swz*`PThaUWF>D(X7qU^cec9Ch+=OAIEbD?j!aDAPFoxYqQC3^2le{->Hm)$16rf z`)SgP@Iv9)cZ|k`->joQI(&_= zhSd`hy_n2+sl=M}snbym*xgG1G~BFtS58pe1>;HF1bA$P{Py8xc%9&BVs$4S-@w9+ zuyf)AX}>?+m&lMO!^~mu*bMn7{zZ7)66E&$Ej(uGWBEQCX4bhUPuTVYQFA1`p$Id0 zqwTv99@DAr(wfY;=Z;1SEu&k_jM{vK6<|d zF9l)ptDtCyEDzHSU)ZgL*Uc_(_02G|7#_z7^BD)wKrh|Ch|BV_;j!K2?JrF>!Runj zLYdZ$7H#~XFtyEs7^Ro1hdwRoRkscy;sNT~kuUjon_whaIj-qB-Ir|J4gjcXT zA}$`g;#4se9?y_+Eqn-%(++EYVVLjV@K{lmTr;gpdpen2@IqP)Xcd07?OTMGOgTiR ziqzDe%;{Bh`x=PneZzWs!x@Gs@@ty*KB9ROT&q_5$bvDMv9fN$i$)yWk)Fo$GrY{r zKGFD;%XrdP^lge1$3Z!_%UKJ43o~or#cpmQ8q@m@hsX5ps@Aag->sm1t%Qx?=3lTl zLh?CEWHoU&2uG1O@bY&zd8dxAm(6eBMj(=0E=};pH+#m@eq5SUUHCUgk{zIJ6#AFD9?n_Gv0H^e(yds4&AP*~jFup$qHIkMsPu%AtA zhSifX9G~g%eSkVey+Y)b~bK2S85;*3=8zkF!7ADK_v>($rD&j44yxN23yn|se zTX%=!n1F9;mivpoZFHya=Lqe|WGdLp@=Xl&t|OthU9So7*k4#B*k@mb$L!?&(O2-e z(lv|g(7&$qyzF%tr#!i0*T9qe9b0|}Ug-*PbP3>&!@heK7B4Y!l7CR@A&u`Xc=jl& zs+#u8Bz@}1@w4}DMDi*8u7!)> zA$qvcud!%81Y>civE1U^BZVtqHukzJ*(dYN!p6Wv-J6K1{-kF(c)gy-;Rs%xos zo{lt_F0o+*&I_C&7BbE-7&{D+y^qBPlA zn3F~NFg-$VX*b2OzNN0E%Otge8OVk@9*3!_avP?~b9^gzT=O_gHU}^B;DK+g*0SaH zh0ix09=8$qOs!;v#CLM62en@ja{LTSy@9eQyp7l!rh7VuBR;+n&%%>Wyt*%!IlXo8 z#*~KjZL8bgPRp3D!;32oy9bZcR)+O%r`yK^>Ae7tYX{?m@3_B!$Aw|?r3%xhy?eUj zk|Z{T|3hWoM&O{*Oz)|vs)!D99pkkPWHSvOD~e~{3&YG)@VGiWtdk4EOs|fz+VTa1 z*WnF8n7!GXS|?c<`E)!PUKql#udl$nD)9QiGpv)N!-P*Hs>VQh;&X<|Hpzq2Qh01V zCL8W)z6=#z60rwW?kuOje77S49;d&lL(${XkccoFdA!=EB}OeT+(<+{pr@&UBj6)rD1y1g-)^WgHDg^g=xn7Or^>=XH* zs9krlz8e-+jh^~He36WP*xSh%*Hd)qj^->wC>x;Lz4`?_*2}&zc2wC!_8D!Lv4;5Y4Z%c(YD9aS-Ws|XVO~^k0%ECNT*(e zY_as>;K^=h%$MQG70*|vzW9fjK7HKlpNIE2cs!HII=%{zy(ow2D|oV%%+uegI*~X0 z<}7&fy3gb4x20h$@99W)2|Qa}fyds#u>+3{8b-N2ggM5*Yj3}ux5RCkAH(726*y{M(;Ox-2dcbsv*POz09z;;K>=ot>zDStUtc=w!)0q z5u&*ZBfEZtq#mMIcRkFk6z7h_$uJO}>~4Oa{VqJYz%fh)o}84&W!j90mlMY1vUfZV zZ-71ECtz{IlDBBpN9y($_MReSh#uyMfg2%Dt9a|oTntxE0G9JTJT4*onCIJQl&;0X zGt@=Df&Fdv-BF@>C=LljM`M8PjKusHy z-|kF+$J*L=62|%jadaTsRszcO_F2+5WQ=YT)#9a_`;W&Alg^i(iEio=d4>FGd*@X((*O``gpto{ozO-$0>xgv0nwRn}mAv_-zh_#a7yzhWC7Uyw$_Y z|Fu~KhZTIRk$Di56Qi2_^;)@PIlVBR7>hV~JY*Xpc7op1M}>K)vA}H~df;keLNUI}U+PAtBL^ff$r4dFu!{|T~^a?mEi;|h}AW_U6tSHcZ= ztg3y7>Ct(jZcU!t$77mQbN@v5aN$nEfnLXxqx8vLty{f4^QWLX{NbmpvMJd%{YNFA zrZ|;@K3)0LI`~&$rOToGP#M004n8aR zSM3O1#E&jkX6UsN$zSF#d>*9b!Pnx4>ux=M_@P$D&kp=ByPf#qr>v6iA{6&CbeuRv zK&DetqQpEoJGiADuRlKsweDQUK=7pb=_~Edf!Vf=XG~X`xwB#C< z%{rs}|A{hvF@9L!SNNg-HGVwt^9_FZp)&k3{*JC)fh%wr;W~c!DXWZ#N49o2o}Ahr zDqH@ubZuq4-|)kHN)#`vtjM2=Q!ITP4^(z#*@Vn148K@`UMl0V%7P;l|7Xf}<5XwQWt?;D50xfPG`7a# zuN&JR99v4YJS<^U{9;bXNOkHKJ#3sk^^ltFV;yeFWn;8Q9-mF7I#|&Bgd-h?pGARk}+RegVpQELZ*t2I51d`67Pt(c|limsQql zt>RSX^Oo`-qzrl+e{0w7sEsOOStWW$@v=&^N%4QCjItTOSdpzNepw~j=ANtDR0Jx` z_moe?g-P3|{C~T)ER!+K{mOrk>S9l>-cI-FqViX?$ z<)^GN-2PWO8Nh$7lLZY{Iz;JErNf|15C`Rl%J}h6qEU)dX(lM2${|UDGJZ0Y6-@ns&rK5mmjM@StS~ZUo4pab~Qg#CK#bK z9?BMtfim7$C_iPDtHGt>PgLRmN%OU`fCZ$gh*V}gN%>`!7o%BcoocVYe`kj5`u|3LcKLP!j`cRQkd5TOjnBETOX+Sx ze99^(AOCXzrrWRL9Z>P8Rlq-oa^pG!WxTU0oJzmg1Ld=zFBSMoMW8amd8J<~P9?tp zWx7jH7I0aGQ^~)F5?xoE%5*;{zf?JE`61xC{~78Bt$OeFVyk8beu7 zQz$=FUS!%R|6kSo2SsFoosf|)G)iUozohvOEPySHRs~aeA3t3A|4y~9|07h!RGOod zPh~|$L)q11pqz9`P+o2`p#1!Il=)?;ALpi+) zR6Y+${s0(R)k)L^7H?$a{u{QRs{-$<0;#Mh zo5L39N)0HZctM%2B9!r}Kw0sc%J+jZUI3Ko>+xwJ6Fi~Om_Gs$XpB@5%PMO)N^vUv(Ml5(|9{kc?Lir^N0L+lRL%(( zlod^ZvLdNa?s!>He*P6@`dk&CT7&1$rxlPE^aYhbC{86`4&@wJsW_G9tI98{EbujO#(xva^y^eSD*d;h_WY;8 zuub^Igq!L77s}w3^X4V?5l#QUgE|)y_UE)Pxt1p z2KG~(Ix1pa6|t-`{{V0vF`BAyDjyhkg)+PwlnoA3eorVH&|CT8P<|eua#SA>@LwhP zH*=KF7DT9;_E9x0tGt2_0_P?<0?M99gmNfdP^O;%<%i0arzoDPbdu7^Dm)#^{BrVP z;Ey(wzm&2oXTxX4k11t5`UOxVx>J5zY#~4($j%2W87HL7Cq-P=2T^@H;4b?kW_2^0lAvn-On9nbFV6 z{{_kql?ncUvgP-njQ6)v1En$C3(5k#m6lgp5z724LHVH~sa6f@&0VDq3?3VUpvx^7GG>8Mj6}R;;~>PbKdFWj>u$csCVJWd(ZE=lrL^7WGjPsLa3#C5lo004NI@ z0%iQ+O5>rdcmfoEv}F8Z!4sg2mkMRN45isn#^--&V6Q(KOppgdyAEZn-O2r?o;+0kAlK{?mV^z3Yf%wWn z{y)5TprGCI{}0|jKyeuV|Nq`UKu@z*9=?CTaew&!!Nd0tI1m0u?;*%-yazWNl>yB# z{NmozOYySG&GDbTcYt6!oSW>!_YX4BgS-qqeE&dACBBEi4te^~+4m0M+CI1CvhN*`)8{05 z`2NAe_Ycaxcfh$ppIh<6_YeN*>jgYw|I-L6{Neit=!l2!A3S{j;NSWl!o&9u>|6W- zT-*4zcd=G(h;^F%T58pp1`#!?M_Ybf?aA7=r|9~4MJ`Z@!Kt4Er zK)`>M;NQ$qK8NVx`v(u-KX~~5!Nd0t_<{nTUp;*P;D7)9gTJcaZyRp^;~hOi7wPZn z{d9-e`K~@fc)zPx5&>HP%8B^R06PeB2~4qX3qZ;ifW}(^Du}6D^$~hSafDJy1a5;= z7I~B^;v~gKGJ0-PoY7J+*K4in7X3(#7eB$%-mpu;|Z z5HV*TK+ryb%LMI2$bNvc1WWe=bP$&a=I;mS^ASKNvG^l^(2oFqBj_x89RRpWu;u_j zS8QR8&M*&U~#E8IS0EY=`a5fDT zc?2_#0h~Vu5G$H}0ub~GK;b6$BB7pft05=Fmh|ZtlSLmkzD?bH@ z7uN``62u$_7$ue;2UvC-;66cuh&}-jbpl|+34pO8_9Va^f^8=O5{3R5z}k}luFn9H z#AbrH&j4zC4&V}rp96S*4seiQqVPQhu!A7?6hNxjN04#~pz&#d$ztkhfZC@4P7|by zz%Kv}6U_YrAXA(qnDGTbhcf_E#hfz$L1zFi6J(2!vjAramYxO36_*I+p9Sbs3@}|R zE(QoK2KbF2PxLwmaFt-qIeKz#f9R!u%S*`)h#ouL0(Xodi1w0xke75UCdcQZ4`#5j-RO zE&|lP2r&C1K%qEtQI9R>sTE**Unu5$gLU|vD5fkDA>Tq4iv^S=;u6IYoxg)TFBVgl ziffb?M6b&bA(m5?iQAOrBKitsg;-5_QItT$vdbtq;d>OkQmp$PAnFQ0rKgBLpee z0fK)7cuVB{2vGY6fb#?!M6(+JhY1RA0K6lL31<8V(ETTX&0@h%06{kZZV+q{oo@o1 zC0KbAV4Ju`F#jijgr5Pni*-K(gx&>>D4n12H- z`x7AjZ-Db+CqdLbz0%SEqo00ZX{v$Vpj!m@lJL{vuf31>vvq)P#SwzIzW{;_fXgD! z0Py}B;5@ zkd$bme0h+oB*YhqFbkf?GXMI?V~!oQ+19RF<3)QZLkT^C0v zh6t<#@ep|wPjQms5X~w>yu=(zIZ;f(KgL=GVu}Tn^5PPug6QnSPN;}dR{EfcmBcjw zoF01lLaK=66d!S$;wz%7LaK_@lxm`cQeDJWgVYe~AoxdTMHh!L+^D6CB~Y9n^y&!p z6Y&sn)d$7wsg7dm2(t#jGGBo78UO)eCqYzIfPk6+^+jq;fI9?51cAb@7QotS0JCcW zG!jP$;;I7#*9K@T@@fNk*8n(A&{Q<@1K2@O=m*eD6ceP>G%AZ8{zhM%C>Hnw)UJgX zH~bMJSahxfaF}3a9e~#28o`X(05Np|Ld5dA06~5L_X*mG=m3DT1RFScJBSj3`ThV2 z^#D4Fb@c#3>i|@$571e}*9W*tu!o?lFdG0Ys|%3c0HC|rNe~qP5D*9uCQ<_d?hq6a z^b&pz0oK+7nB5Q{TpS^Ys}B&|2%wM1YXsok0N^}9q-gdCzz%}KM*yNlF+oZoK=;M~ z{ltRC0JR$e+#rY%otpq0CRo`7V4%20FryJbOjCeZvAiij&?5l%35JO1Ab_(38-f6a zi4ubOjR6vx0gMpqngN710jSg*AYQ~b2e?YGhhUU2TL3I;3Xt9cAVKUThzbG-XbCV@ zq_zaOLr_GJDExu});0r}9So2pju6B(2MBHj;1YSQ0K8iOoF|wlnzaVlK~UHlAXO9- zq_hO+-UeW@SkMNbb}+yVf^^Y21mG~i$`F7|agAU`D}b1`08_>Cwg5q`0qzrIi|BR$ zX9+g61IQI61oPVfB(w*ZF4naN2n_+K)Bzw*#CHI=O0b9EQDJrjSk@LGy(7SEv6CRG z9Y8=QfP9hK3E&Pv5y2ec7YeYpJ;3ZxfG5Neg18OtX6U-CMx&Z7T zDC`2TKok?CbOPw!72p}MpesP_P=Ff*g`#sefWrhUy8%2Wt`W@W3=q>DV6j-<9U!O+ zzD62(kZ$!a=vV|TZcun|)1H|mwV zMjRpV?gbDW0q~~CivZX`aGqeDXx0ZHr8hufAAq++F+uHcfbLF!4Pt>4;4r}rf_Fsc zNPrm;02^+jW}9_!_cm%4)CXi3$rfGoW?j#ctc*k=oH3Yaz7rrO3Shff9t98@32>ia zr-+UQxJs}g8sL3VLa;0fAfYe79=R~xfVF)A()$B^ zBz6+S^#cfq0XQg9V*tGS0~8S)5`F^!b`Z=S0B}ScAxMb<2p$MdE?l{K>#-hJ{Q*p1Dqv@8G?)7Xd>Z1HYkhtL>=?g(F>tYv)_Xv=H=TPPE#quGj@(zOgs4~tQ z(L(`J;sA0H!}viL`=3La+VLPuCmNAne8_S`7w@Kl9wv3Af%2apx``6gaUH#-i}BRk zD3AKHE~;igf6>Jx>aWO?`kOBNGoiohB8&P5`ipu;7Y(OCOVC@?ySn&{`X~BoD)gQ% z9;e<%Pf`Cu|71b`*2U9M95yax;rgwM&e;${ET(vfYZOn>D+l5b%PC&sHl>`1&V_i3 z)f7{dKtxIs+Lka4ol-%pn+9G{=+hyUL_DRk*i5M+%oz|Lkx20sJ1JF#ZyuzYNTpO4 z`zSSp-%LnNF_ltF9HG<}fsaD`L>|RooTStd&1Rtj%TiH+!da+5fG7sQ(~6MUkosZ) zrGdCa2^5_lgESP2DUHN6h=@x=(#DH0evJ)r_XJj{cRI*y5S%ZLEJDm65x5xAOyp6T zi<6WVqS+EiOEHHMEc7%;D^X0ZwFt2wZNvgfh`2;)D>^?9X(tv_+KX$H4x-mmNJp`p z(n;K=go^mbA)Uo$N*7^10qH8DUx0KIt0~<@38jaK6_7Boj?z=;%OJf(Jf*kTObHj} za!7J_l>Q>{B}k0OqYMxyDFa2bm5@PV z4kcC;QwEEWmmx#M0?JTvi84%dUIiI07E?xuYm_+A>lH}6SWX!!Zc|2y=+%(XVl^c} zlu*Wq*jFKA#X8D3p}z)66!DbtVlyR4n6E>UMIyx|c0z>r6BxpPH5kH)B6SVG4uT?r zRN?mqB2E%hDU-z!N}33K6Ou0SC>i1;B~vt83z;J3P^OAvN|p#&2gw!-AmZ#iWP4*B zvdtBp*8|L-53q7Qz;tnqAansh%v%6?V)d<$SS^vOxH4 zhCD5%Ql1eR&NK0 zdI7+(1M6{>AqMZj&UuGq8_8-z7&}4M3J}*$kk<_HE=k-nkQ%!{))->^E)ehKAO}g_ zG(^?+L3WVjz7Mj_5c^3|R)E}?h{E3zom0>kZ;Qp04dNPQqv(|ic}Fa#Y!bIAn?>{_ z$h%@SWs4}GY!$JSA=|_{i1D5w{C8uM-EN31>JCF3rS3FD!#ybW>M9gFcMmG^z9BvX zF?JiG`3KNFhIpL%fg#ROKQu(!521Sv@icXxA-C7O;yU%9A$sqF zer$*p)I)~&nR?g|efL9;7~)l^NLhm#I6guRj-iu30;&B5$TpH9bn*dQ8a@@v^U&los=`e_esux^~g8(N#t9M){kWC zN#>3O`4X)sS@sr45#r!6f!|!tfwuu>&jq+3jsWNvMc`AAOCpc*jW|j9Ry3Oj`A*EC zTo%O;;r$MhcAt-=--`wF0d^4FAh;$vF91l{1h8@ezz^aYfc~TC^)%#$SWfv#+@{wjgN&PZhsl-VI09&yv&_0rDH>-3XBRTR{$z{DFBF2NJpsBsUJE z1oMvMDoNvbkUud+;z5?Z2XdO^KBmGbkf`k-g`+_J#$+J5L(+XT2(Hjij|N%01LOvY zhll8r01~$oWMu+~!$Vvr@!kazGX|ubhgdNNWCzK864OKU9Sf53KFEf#AQe2sU6R_n zK@!G+RPqq($AKIssgwv(#Y2ot1evi1WDkk2hbTWDB^zk6oJj5=Nvm^mYAT>P1 zq$H5}AA%H-)bbGiJoAO_1(}_U0r3;@2hhz|3HBTSs3XjS0L%6Pq#pzb5IYH?_5%cb z41mW79|PPWC?ddPghK#pKLVJ22mp@}2;vR^1Rn;#V}!#1-Uk8B6W}qz5r7>8g+~DJ z7=a+=V}R~Q0q_{%C_wE)05=Hm7~vSeVS<&%0Pq-rV8&s9m`~8U5D)R{Q1;0YkToE< z^56ahQO*(!D+1^sRu=)xKMLUZ6rhuc{S+Yd7{E4y&O$#9aFxJy9032{GQqM>0BW27 z=q?gZ07Ml593%)6z9#|h5agZ&=q2_Mto;*p96TG0GRtZ zK%_WHu!Eq(DS&7(=M+H7Nr1}){Y1!VfZCq{EIkbnBQ6mfCg}48z(BG13xFA)1N=r1 zD|(#)2s#C@<_y3Pahu>Q!LYLc!^G;d0P{}+IEn#Ah}dF)&@TYC5yT7q9Kclq*ExVu zVl%wDchJUom%&-e5G{o~{1+WTJj>Rx;Abwn^smHSApwYZPq>j!degvy>A4W9=H zMkPmMl)5N_10pD#0>KzHECqt@4-q^S!8ldG6+yN~2xht>n4lht;DQJ$xgnUOrn(^* z`WV4i5hztIC4&4<5Ufb)T){e(4l1QHU8qRvxg(iQ2jz}r%2On+9!O@=L3to4`wYo; zk<6xpN`>TuNJ`x#`g7H|n^Fs&BX}!<`Ks701T|kE2)~YCq3U-XL6VmU?uvlTf@=si ziQwur1WVLZanj-yg0Hs_EK}ufBk*~RV8v|&E7V6391uahI|%+(OYR`({sw{TT?DID zt-A=ay+yEH1Z!2&dk8Lwpv^r5>(yov41I^d|2~3^s^xtI`QIZrDuT_*_W^Qe+h-w=Ei!BJK2 z8H&f$TrrNT4&wTR`Y3lNRki0Br_>TLPAjJu7-v*1G0v*hVw_V+Ut*kBjl{U1Hj8ml zdA!27q*{t`S?vGpaaDB?!cz(RirFy+_a@34*sGc&v(jK;V-U zL7P<6yr&F=Qu7Xo#6LAv`nei*1C{Q{5Ul@%o0qEECj{A&BWU`Wq2o0}Woq195Xn*8 zSl=>KdLkK`0!bfFB=4QoA(7;FMdE@t){o9AgBOwqB55Oi;b*n^G9FKHL*Rb}!B^Gt z3WBmJ5gZl4cjZeVs1G72d;?FMZ0gnxMycbhNeOA7H^!=sA9ed z^7|n8>Wjcvl}n4@fe2QlMc}7CieQQ_f_i=k{M8aa1ZC4AaJ3^yuWH#5d=SBQyK{+T zP8RJo4ojd-Ww^;=cCjBegKo03m(iw9iKM0-N%$?s*4Npcn@nh5!lt$rb57#2fY*jS zHU>sHPq(^uYTBl2(~cb9!EvFk8+U~}FSClGOL02~v~BE=jgTkjaE{a*x6g#*S{XQE zdgE$;=f9jZVPqDByRxAzKf4m8$wvM}c12_(b8wc2I4+?(wr4{)K92EK>7`|S!v~AS3UCSmMy2L3vS{(N{OE69=HU4G63}lPStMyXt)(g1%ElHslrUy#lZ`vVowp=*i5!SPLf)!H$F)_brk&8Sycof; zX_A~?6N0PTT#An76f{w5tofs5qVF(%bc5qcv*|}*a0{jPAGI5tv;^DGR@wMxZ zJnXJz&9>5{cs)O`k^G0A`cg)+X0{QRad)Zvubn1p8XOF%$>dkdcwR7txo#pV8@gI$X zdv4R@O*^%2*_Ql`<@YAEnvz~G&{myt_h{6nwWWF*!HTY6HO+0l3pU_ytcWMYM0HqFx4xY8L}4q|NGAWMBA>Q{JVPtTKhvm{Fj%>)9Dg^LB|lvc~0eZp~Y&*LNJ#v-7&19@ZnGhihZPdAe9*ATkntiBGTt7S)UGjvNf4T;HNS*NX2|4_;{%dx z{!$no%7Id$hRoHF$w|K14VjxElS8!SKy>*_X~^Wzs@P}=jX!roDA{NJK!%4Q^Fr2| z=t~ZzGGyLd%dgHzZlpG3bi9^hAb*~Qj2_b>C*R4RoNp|-AV(YiYRIC!4WX3LRSAK= zG=@w*M7?jwd<+@mxuvp^TfT-&4m}^twfv^ps(<76N%Lr~p4h9-}nUFCYXnz?DS!V8hjTC1zWLc2) zOGNq0pLx`CR<3KIC|Q)*P-ISGk)vWIm01i~cCO`&V)@Hz$Z~LP9xt5DkmW=s$H_~A zvLlnY=7O|@A(6^y29lc_H}XUNav6?`7U6=wHw6ahRq&;1R^Kc1{z{CF62bnuFws-Ll5W)y`VSrfxaMT{>DH*=nn&6APj=RFa(A|K_~=~P#B6p z6cmMGP#j7?De&Xf^Aw2{@29WM2DI|j})WfZ?4YtD$ z*a@-F4CIGHnnMd{3A14))RG^OkY6K_UnqG<1-cFQ;R!r~d+-$Qzyr7rH{m%vgIjPH zUch6x0VVLO6qJPGa0b~yxCn>fES!LIa8iEz=R6n3;5b}{BX9~X!D%=Ohv5S3g*_l= z>}-LJumd*3HrN2$VJpPKCfEt`n?lkL%a0h%fSE80=1242Tu?9^hQcst0WF~w{0Zfu z0yG0Aa+bHGj9>b+)Bt|)(ozd}L~W@Jzr(K}Kfd(~RD}=-hH!|0PzZzQeEgReazbth zf?N;?%ttL5App|CBbwA>SPb{zJ}`~89D$=Cqs2U!19M?MEP#bD5c&d(7RzuL3L~V$ z9?8WR7zM*%9E^ot5Ci>S5cG%MFa!oePZ$8w<@SN@&;w+q&H}|^qfc}O`Pfbl0{#Z_ zt0vFj3)gZ`as4E?mID#xFvZ#+r!`iB${@dPaStUq`dCgcPK-wvkzE2;uH7I7$Z5by zK~5}AgXsfu=t3?Sd2oP+al z0WQL2ka6}F+=e@F4`iI3h?_|;85B%`X)qmT!EA{BjN(`L4mMJq43dK!EWI37z~3+( zW`LY@Jr2e~9T)*}zIQXI4-KF;$e>;Ys>3f(397+Fa%LQifRS>D{CF-V!6+CFLtzZa z3}6@xhp{jf=D;8r0`p-eOonMN3nsvHm;rNP9w=A2DkfO852XZYlfo)vx0QnV+CeRS%IFmZ? zE3^igop|8x4i&LVG&fbj4O~Hf80Z9?gwt>a&cS)O0Egfo7z3k0ekw_RZmBDDhaS)t z+Q9-?2mv5J%jFJkAisq5iRLW72^L+Mo3&i52024*AuNKwU@VM=Q7{^{(Y&6>NlcupU;!23QVDVGXQ+ zm9Px{h8f^O4VGUflEcOyf}DMNg11WAdV9Jnn9h|6Sc01Si}=m!m;zD(Gz;=whL zPd-B+2ns-c$PJ+o26-SD@8G~2v6j= z*+=jgc7S%EE@nF+(I{#Eg}EpI)t~}Yh3fDNRD#OzD^!7sP!7t2{BX<`*a~t|P&1I$ z6a)RBKMa6@FbD?25Eu%>U^tABACVc!#V8mJV_+eT$lmVVL8l# zzhNHChuJU(7Q!M}0!!g9SPaWx6|9yRuYt8N6BfVHp1s>o9-jD`-pc2S09WDbo5^*VvgYhs1`ao}p?#6!&pgy>OGkm3K*)YFjS}+qs zBKX2BdtllR>u$!}3tM3a?1F6|KfCja$~BnUFK435x#n^#{e6&gwM)VRt{1|hM3ny$ zE|$VFSOu$L9c+W`umg6&F4zrwVIS;=LvR$1!wEPEr{N5og>!HoF2F^oMBx2l1pEoD z6I1?exR_6j?@+0(!6mp2H{d$lgv)RXuEG^)1AjsbXbml)6*PyY&WFYp!QjJ5}GAEM=a`Y})x(&FGf<_Gu)@|$gPK-*ZZ(_t3?fsh6A zK`_Wi(m5a}>?0AqAu|L*a%hVC)3!BHD12B)cD{c%_eJ<;g<}x@&lBa;S~1=K-LetLDmqlAnOBJLr8NS0R5pKNRGugv56(^v?nxy9?%&6 zfJR_8T^WXDIF|XptOaCEV25hp3sP^qRkh!3>7!k_OaT^f- z3vR$okeU7okXAkyq~*(?Ed#X-+J8Y!SOnEz0nCBfAd`EU=ntmGX|wqO-1G<9pQS!f zkQUhmsROm)clZrzKxz0Det~R|3@9rLg=itqE#$4F#X>n~g(7~*Miv!H^H=V(LKetu zQCX_nYP+Q1ns0@pY&oMda+wLLgT%Wkl!B5_9EyQ#yqHnUVO$r*jD*5a5b{Glh=5SY z3tCc0S+~#?r80;I-Ju(Z~up7i~4;(SB z#qN+Hi^V($2Vg(!1F?@a%)^*szfa0vEX0#za1}1V88`{2;53|rvv3|R!WFm#mkm>L z?&P5724@EGpHJ$M8U;elaFq{QwvyoL|(89u>B z_yXS{Ii4qjq>u#636%@iHgEaf|5`I zibF9d3Q-^%H=0w!ER2~B0w4oKf(*=spaA3t*$9$NpzM$yvO!kJ0-5D0hd&pYAS0L_ z#r0Gii9FCS<=(Uxx!g`a+@AzTN@rd<#hvY(X)WD867v}A8f zwz6bjE7ag;fr+ODA#Ols1SOy%Ag1Jm(=R zWFxOSB<4Q5S=!wH0;WH|8u!tr!1Ty8>%~zPSwj#HWrUT=ZF=4mxy%fjKwU$pIy&9C z?g$;A6*L3Wjp$ZFhh0g2`PHy!VVI&M$nJpb3`ixGN!y>$&#;qOpvcYp_}8+#VA`2} znD=HRWVO;o%D)pArm%CI>#mxDDCrX@iM6RG+1<;ym-Ub2LR@<(*1k~2h_FOZa>dkp zW5}~h*C3u|11T$0!3@Z}zlNP;nJla2N)nQyH1uk2dbb z%_xZei~q!9(}8$A0mg$g$#Ec?H=-3}T8BhT| z8IxvnJr@>8`OoKKA*_PMARVDxuYt9&6vUGyATqK08#Z*oPmpQ1#ZGsxD1!z z0-S}@Agc)RUse{URHJ&fG|}SpJe&gwM!XSK$wavqFGWXW5}epy16hnb!MqN4;TDL! z=!%;bxV?jU8y z5=ab*zy)mJ3|4RgnazL4zi;przQAXv{Q8dNM{YjAdw2(L;SIcoSMU;EfTUI;nvrip z8rXuOWmiOYM`V{o)`N1`zqxZFyP_#Uc1B$x1;}nGskY?8%w}9?H_V)vIUopRX(tJm zj1hey5BFk*V;+Sg@{sFyF3Lc8s00-u3JyULm;z-%K?sOmA;=4%5C-C5eu#j4Pyh-- zB$S52PzuCfaU=eggrf3Gl_j_+4#hyO#Xx9)1Br|{lE{<;iHzvQ4Ojva zd+|eZ#Eg)*mE00L39AB#y+nu?{hv0lWsH{kDUlaHYJ!9%xnw3hGhsF7z8Oe;Z;IIj z8p9vZ2pU2Is1Nm^F4TeA!1GM)8D|&F&d>=uf^5{ax3X4h$3l?xRr_{0gH~9Qdz_lJ29nFI%G5}5t<$?}R+yErD!DEBQP zZqAuG6W6WSM@wKoOSM>vN8&KP=OP#TgxtjqWWS*s-wz`~(Ut>-!jCNBUK;&jh?}B^ z+~0${a0g`ZcN_B-+=Lr&9i+}*!@LSt;3Axdb8s9o!&x{DY%dPQnQ|1F`@# z7lP7X%k>4!OK=$^5)a@$n1M@R=JjK)AAyXb!Yi&{!V7o~&p_;-M)Thj5G4uJ49v$_ z-}(HAoxGwUuWBTQMBoCl*DQO^vU&Cmov$D%{Q{ri9ejef@CKyY6u;hs*nKc$(V8VU zPIx3UKpUnY*W$qRBo(q0Ap61czKHo?!+hu|cJlC3wtU6Tl$&O1WU?ia2Ga|~KN1kF zc`Ez7iI^zKV_bO*BTrFAf^0;O0D0Ea3+jTDpKR4eg47d#qX((kNIa7)2iV$6%|0+ObkXTiRY9O(b*j9z#pa%R7 zHK7)WXA()7HudDXJ9LBkP!B$VR65C(uFwTKLnr769iY9Ge>*PPLK|oee?lv02`!*G zG=rwl1R6j?_yaN$fkv3J%4=+xaxFb-5AKITe~@Aw0#bB?p&tx_v{L?3p?Y&8sfmeW z*+)~*?Zr+!9e_+yES{RWQiOd${4xCyncNFfJfbVt12Ls8m~O8U{twS2hH(QGC4Z2Z z#+47}UJ;3jAX{US#iL*bOomA?7DmGa7z5)#WaD8XNFA7h8LjwlDolgv@DX0Y2Y3(f z;4Qp?*RYcSpJ6@$sf>?dCCmni$U@i;bKns?ge4&I1@Hjw!#&t9NJqVU_vliCCYLLXN08!e5y~J0p#REyST$>S+%#nMETzr!Nw33iS zh`wZy1irzL@0SK4MJFChshB0P5t(=MNLEV~ks^*;WM)dG>PTuOrLsXS z{=~PFaHF-9b0G>6h~z@tK;zy^prRu=CUS{@xR<&qrbO>o5Zl|8_wvh@ZD{FaG|8Da~>Z*W$mtjCV=eztmRoNR*`( zi-H6!0mb!HawNV?+=*Pil#op|`HCWhdl|LO4YN~R%hwR{24E`ahn{@kAd~PS@}GQk zAOoQtb0+tGnDP}v0Hg!?*dPOBf{bvYJ^%67wY=KV!8Td7?P&A%YlDE{C1zZn+M}|p zFI*srQ~l6jO|83{-O=XjH;@~56ozhHbjqjO$y_Le6wnkVs?!~9VNSDD;!d`H;j()7 zM(^r~e-;Kd-|Q-SK_U5qg0+%LjZN-lS3=wLZ+-+D#6cl6Y_+`;KHOF>JK21#?<^`s zXPb{-A`soO+;8@OrZbHLD%J=J30r+JyqA1zZ-rX!KH|=N(Wp zGI^;*xzNiesR`i#-;`a5gKT6=ByVQ>CgxZ3JWF{0oeFq6PqZuZIf9X6iBfKg@gpB=|s` zb+eYgOzhB-MXf|}#pyST9RI;-?OD7*f6l?JcM<{9xVe;4P;c$GgWLdtF`HlEmwYSnGx?9X2ae8=U&d zj3FrsW2TmA@yc&$9u)Et3u3!V&Fw*5KBmt0Af!twV^6}pr$%7-Nzd$stFtdJx}|zi zr68_?gTjLH(tj)0UPLpc%GwLZzN(JgWmH{y*(&+Ti;xmlFP}o6S`PWO8LmPK5LmD! zLS04APhMd3MPX3OrsKRaSNGB)qot#z^5~66T~$sDzhUS}UCnuJ;l2jL?|%}HsH#EX z7Nr`9-U`*Dx9y={4Hvx>2lqJhIXq_PVf2hF?ymauv4yEaeQaqt4(@IrTTV`2@#ssV zy_!hn>T3(&%#BLiMwlPmw)ogP#ul}{3|>(0B!BuR*30k4F0XF`FSNL0b0Q1!5(mqK z#A-%gn~(i3G$ix;9k*uvWq`GhLt|@VbpnkDd7)MEarcK@AO1e@be=;)-bNRj_KS*a z>fh_}Y=_Oq#424hy-La?djC}A)x4d}g1V$}Xk=E^qRE6p>Q4;6YUoMjShBU@qxt=t zb#>^qP_t2ph(W;z1-swJ5qHB&udXq;Ws zV(gCT4~y8Gq^?sBERV49$EJ$S&%aIb-HjYJiPfln#49sZAsq_Swk^AM_Tzf1L!pFf z7eo9TsVMa9G3W)L*Ys*o+Q@aIA{}~jlByjsYjvL|zizGU&`8+EaO;<_ z?|SfjYcG-cFR|5=6OnLw6V4+e7p)&W)VqGR7{3;DEYd*^>$kFd*=dn?wfY|{+1Xi* z>rdtEms&0FPt6*GqBMX*P9B%4EPJ3e0NNj2^32rgMt@t5jLXo-hDQ80=k8?MxT)L& zY{#w2Q+*piCN89=ikG81q;J&UbxIebtXMzwFun?7jK-UO0TO z$7x-Ze|838iCFk2@3xIYQ{1#V<)C`W-$GAi9b~It^B-!j^tdN5rGN1r6p>_ZCg)aG zr;vc$0bywVI|&yJ-6mnVJGX4p)Y2ouee3u^b81+fZW%U@eblBwwu<&;^ho0C&DNPO zEG*ySknSsKv)oIo0tS<~SOzAUa?~BQce!)Gmd+$0oWxNYhp~~ERL2L~T>SFU-XyeLL2dG5tdsGIFc;x>0JFEvNLW z9f#Q}X1pAz2lelYY1+(CH;2*um#WVg_KRe+WZ~@0F}ZRMYWjz#Q<1{q+Qj5T1{FM< zj?g8e-p#s2Z}b_MqepA2Q$!Hs3MJ&L4iCqRdMHQ_SYTy|67JTKW2{cOkx=(y3@@yP z$#^9WQvbw(dOzG&!zoZz8bObFiH8#sr_R2icR!VO+p5*sP-%a6u#wt4|46w5oY!4T z3S6(}-Lh<)F~VkdvOmb8=YA8<<;gcxEcp!;W(U#~US`>NW+W=s@7dI=QMPrQOT=jA zZKERbBNd}fP+Wvs1HC9fsPxGMGy7ox3~bz(e_y zf=}zWJeOXz^88gK_THdB`e?Cd(#xD))f+=E7?WF1c+{eYUqaKheXAurL^`P9Y8(n2 zCAm!WG6d;kW5@EFpXIucdzm8}!qoLKRHf)V`Yh_-(T-M8V{L)h5@3A(YmUqyQyg2;Z4{?cg`Dl|nR7x-#tdtsufW0xfBNeyPu(IFQEzH#0Q7&!5Rlo!~4Q>_nkh9riI8~IgT+*)7cS6wIB zLaiAJsU2^*H_fSq)HUqw%M0n#qpnfodlvav)rJq!*zw!cJQ1mKO{R;w9I3~%c$FGe zDvg@3Lpm{8G6q|^s|J&qi1t>twAR{%)fB~uGrWkdU8P%&+2JSumL5X*=cAvONR)HAU%!!ey0_x5we^_IBi??e;M zgp?lP$Ok-aKaHI7FQzx1K-ZV^J|^;?;?OYsP}`;v*C{2`g=u7X!;&h+bj(&IRnh4- zAE!3TX*T`%qmp_!N!vyK^QhW1y4}7JL7~!QT9s0xQM7g{r4~;ov6D-ychhY#_Rg$R zWbROI|BjL?&g>{+bu!jPmcwP#_!+hwNgtHaUOSKJtgg?X5o<=6y-GPfq(Pq=rV4a< zSf2Q@CJLfG)G4QeW-{h?Mnn3iOdihbb4+bpK+~XB>d$|qjhX8|DeC}jy?;^lnrZW` zI@oZUBJtvR_4EF-$=#`sA(?_H-Bil!({G22ZrIJ;X^dgB2GawZ^fRqD@}^pM-`#1U zp*x_wx-*l!np9pLn?+X7DX(kRPjn`H<4b3Kvq6hkfeNZ38rF&xRI^#Qz?>$g;nKRM zoU+fx2j|LqSNLsNqqj*)1s0-{WO!nwZ7EhoRYfCWR299Y`)9bBF*a$2%_N>#hE#lo zjZ99SG^t(VYGkY|YUzk1Tjo_!v&8MNs=C|SPBSi-em|SZhdEg)QB6<%o`HR;SI?2h zMaxpHF(0p{o=D7Yp&^}g@;XPlENEAH8htCX-U4*!@2aVQIryHux?a&-H~dnzO|G0t z92yz0@sS+7GicJ$hXJ)5HU+Dzw&J!r8nTudS-ia0^BD!MJ2cuFHs3Z}UUhEWv&{~h z;nmeP+}i&_Ll!e@a+g@TwbQhm4vl@))r&c_S-lQhSJqTT=F%gC*3#=wuP4oq4o&qq z1LGS>p@~~`MMK|7+m!INeypWN;>_++TVM1=Z7+~wgiDQiy} zCfyEC&Q!dvl)s@-1{=wNZMmF=Jgc-J*kRM4wsMCr1EY8(4Dy-%ft7b#D$Yb4l>IiP_F=)t~w0^scV>bTwVK^EL%hDz%)tkH!&zBks zfleQHE-m{`k64Io0`0_xU_);`M6Xz}{d-T`61vpFRLF!8c1tglXp z?@!5S4}8yl{rRiTS!SJd*Zg1uhD_CM{;FB5#reeja%nbsCD{}~xa~h~QhU~@?U1#)SO#FG~(Dk+jTPRdhBT{@beKk#d*Ib(S_^=~RsP>;0 zvzB1f=Qu8l5(oS4KaB45Lf?(6@ANmCon(=zX@qHC*;v>7@^SaNhCMu`OAgT|NqSY3 z_-}5ku86N5P4v;NW98L#S6^FM7G3g9{`{LL_a%&Hgna>&r>~L!TNML3jQ>k3Eo3am3-Xlv=p#DN*u&S#;F#c9;^yk)-V$` zWP?m>?zdG%R&$k_?QaIXc;f$&$<_%S)wREW zbhH0Tf9FoB^eS6Lj$@s$%9b;uoCxVgTK*kV(`K}~xr#~MA@x~|{p#dqj6y2UYW5@A zcUE;*+XAd_I;*~`>1w{>TPl1j`MW(!^pup+DChy{Nb{%cqW8VsRvo`PcI}vr*o4!? z(UE27qK@F!8s0_STTR}X;U*j-BM$oJN?<5+rm%TcrU{OZG24^ z)qjmGM>2C4=tCE^fqNJ2UR(3Ux%{D(teZ-=mPiG5Q~B4@h9kPE*4+D*?xyz;g$GwT zuzE_xnp!x7#7jhK_F7xcWYtihBE;!B8i*3jYwLhN3x`w>W-bl9i#;9abr7VDHAYVdkiv0gp(qPm!P)cxOwj9N?} zGCODTl(nbYy&hk+tuJ5ez@Ex|gX8viPgQ>dbN2+?hXh*^wi&IWH)(Zf-v(RGO#i!} zXSP+I8*L?jREcf9^~!o6!X{b1DyDe4UrR|p z#&aGS*Ut1+W4Dpidwo?~G$T^+0A2cm_GRk#{W5!OF|FQd(}4hNWcJfN|J7${*Lfw= zDvP!r3XfLXw%D3kZ^Wp&;@8s{Rb(rnevQ$ytz*!u(8k|?Tcx>RIuvY46|FjMrAZl@ zR+s)NR$TnI(A67U_#N%9&v5Hsd=XT6PTFDk$+Oo87F9-)<3+-1BrxJ34rSsuW6g+> zAt_uuaLANVdWSFANQd?K!L(_=p1 zu<3a@z59{IxqBF~qX#-OSRKI+&dR?znaOwKaMk`YWqfmp{+Q=p)t=6kYD|@Wn1xS% z;y!n%-Xz|&o!s9u*`kctNQb~AG{>;XJ8_zCm_Flq^5KuG4N@n`i-JBmvh2eqspMjc z0N>>OGlybhOhOk9SHX%YJUa%K)x%YbofNz-^RdhK&e}q~MCr>Z3qO?^Gg0ALrsZ;^ zIw2vtqTw$7w(2!FX~Bru4vnAYi00ORgi04nbxPQGBYbP@2-O-*>-iCCaI7sfqUT6` z7T&O2`7Q6!sn}@q^SqXEBh@qfsCu55%A&8w%k>v7 zM{VnihCIs+&1*S2N_U%i-=bf#`n8h%EmA4(3?y6>W(?!zo@Inu$u8K>>jDKBVeuHF zSGqAv?_MtKeoQJ|a8QAu@K8(oF)DHwDVb;kGYqTm^jMSN^Rw1&>YfZ8# zH1^%XIa~al`{Q9~ki1J`sW3_1*-Ny?O;VxzY$dHON_F37`_<~D)UkbJS~{gC_Ykg8vO-l2^X{w&wdp|MV>fc=EBU#aCM*fy@dpC06jQf(1NOk@fx z$w^dU|LoJeXwf}T4UCX4@Q`8IbJ;xM%uXe`F z9>E^BwmCa|FNuw`5WA~Si5h2m^MJ`rs*y>m_z9vJF;&$+LBX3jW~9MNGx|s5W)7M; zYuX!S@xxv2>8k8WTghYvr|aGK=;>-~G8ZoueZ%H&Ek8rel439&nVuP*+Y^4gG;)Rt zxM}n8nLa~b8#p)ceSIe7^1@nmq2yG}gEqTasl*(6kOHWE(3V!MJZY=^!zQcR=yQse z9`Wx?e=$oRNnK{^ooU}qT{<1ee>PNmaG`C(ddyZ;Ps`(gGh|p}z1z0(WZ}gbO5hmI zWW2g|ZRgBoq1y|{YX)>C%s+2nW3nl zIyBN@Bg6YFzu+yWA2zP-unC)^M&j098V%XQa7|P4;jEQi_d7J|&QY=AdwVn_W-T{I zF3y{%%{_-ke1Dcf%nS5!N!)Edv<#rCYOAOzam2O$DV^r1|4vn4Iq`}vdbKU*- zW_wu0VRLSds*79uBg1X$hV#Ck8@IWbL&GvxjTPTL=jt)LKe_X(xl!-7IyACiBk9id zXPU&X_p~bQuqiZGT@$yJ(U5e9di442=9cPzI5e7IBl$g{(36aNI8EJ2V!|RSnUI*o216OOkjL$+^jC;t_|&VZ&`i#UmT$j$hZ)VMA44 zd0wU%OY|1E)zdpwy)je!+h}p&GOSppEm2=2{0(TN$M!OI2TSTM3N}xE(R(>x_*VJsvqU z+F_Fsn-BG;_RTxv(07N;_@(LyZtbhk2tdPqOr~coMiz?`?un(7FG|D=g?w38HlvoQ8Mw7CLc@+m|5D{n-rcb~%%QP+nK~i9Uq>UYXjGW}=i?>GDmpYi8g5hG zIvJAd%i%8$8?WUm-6fk(aOiT~?YqJmYiEA+ptnP#A~upC#dl8o+^TN(mp1ts6K5bm0H(=qnNgo9KmPB7) zd4IhH8?86y^;lK-7SFu0D%J6pEmT#%$umyg4dk8oO={6CrtzgWE4SMOR%Nqte?*4V z+N^g|-6nst=J^t_+UlguWApM&%VyQ^4w@Y|tC%O4F`Ly`aXEUk3iyo6Ik=Se5%|~Z z4g;UPeC=?#ZnFx0MOGd}L)z9~+dux(W%h{U4vp*BNULx8vPq|6{j75wHt#m8T!dbFV=>M}- zdEX^hM{iThuj295ZF=6Fthw|=xi_Rs4 z=+m5@Ws2RdQ~PBIxy|B2=8Z;Hm_b{Ogj+Z6n0yZpcI{BTiJ3j?PThm3@YXKfW)Dtc z6eN?AygOCpd$je0H4LxJM4N@MFjf_PNDyWWjJR5BQCJ@tTdLl-%`nT@jLx%I|9Nm_=dcT>RJ>MPufV zJ>lXvvP@li$fNS!L{R3YgX$h=*so%;M_Rs-@u7K*+WW5vn%QGU#LS*SyVTgPMBL1t zYmXRuO;3z0HnYb}tm#&dQfT~KG=nm$pQ&!9HE6f0_=I|A=9OV@FSf^6g!omi+xKLp z^|l`kuI3(fjD{A`0S)Pa!qa)Cobu=NxekpH*vKTTP_Rqs^o^2NaM&!`qwLgK`z|!3 ze=EBB_0ulBuJ?6lT-~Fpq7m^44e5zi=bgVZ`KYK&4vn;X_3SQ@bnCoCtv$yXuUgRG z#LF6cs{Q)(sojfiCB29L>P*2H&pyqJ|IrfkHe_Zzjq{ghwlI4GJdlaA&x3D46~=V= z2M_cI{T5?FZ*8+*ZIi|~1WifM`nwNm-u||XF^*-p?8I8;?^iF-h}edPWXj}~la6+M zd~%>e<2*K6SMK(D@zqc8HRezn_0g5?-%1}j6R@Z#rNfC$OiN65~GLhJ636u!;gd5 zNSsUMvSgWa@nS=V&6A_b?FDXI9@G2f(o4TDv9B(kk{n`wDm{4@Y?9&jT6nb{_0sY? z@VZTCxF!0Ss*79u1T=imcv-G;y`?Fl2FPPp-qQ&Qu`D~L#)|K|4UJhVuC@Nuu*O1% z#wBcgaa$wB!$D1J^(^DCd3{V>6SwZi^mFp^*g}aqFLU_QPdc1JXHc zqK>OvFNs+#G^9ksD`4EaJu6+q<*LMS>K{BYxke-FuD)QGW18IH(R;cjLn|m&?`EgpHxq_c4RgA{>Dzp+s!r#`>*uH`%l&jZ`_yGi-8W(`3(} z4S!EMl+0n{cS*Izt-Z`8KG8*E(As@Vs`PtO*rCzolA0sF&qX5_8osMMSL|K$jrptu zFTN)nXYXt3nYjJ;yJ|t#^uenTrsU@1LX8?746U40D~WI(NrY9orXt_c=hwZaetSzt zY^=2GpJMfv)o#l1lzoR*RPIjy;D?Me8H_iW;*Gh+=w?r#!jS^!o2uwLqUm*0pYEr> z+vx9s8Q=1F(VR?WGi)}v_exT!`oJy@n?g6$Xx!$jf`%mI;P*Z|tAt+7BcquB<-A&kE)bi)81w$MvyA7Kwe|g`o(53A)hs`A%_rbCMD?=r) z;;;`*f>zges3f~>QLDxOjJNe}ZT92#=iX)7cEF*M@FX=7$KS>A?>Ls8{^RB+VH@jZ zY2k3(5gQrV_Ktfr>3E6>`yDo;aa`%6EscFSD!d09bL6sDaIv&a;y50*B`ab5!}zKyXm#n@IdwdWXo&zrtu~O zrj9uswST#*SLTJo=48xKc^+?+$*!Vwpk}{oP8kyj+?+>{&KIBA_cPRtscZbWnc@DJ zh?{YK^@S0@49bkOd2i0x%zJZ^_`mex#l>Z|D(cL%I)2Jb?~MszLOnCx$MJ`H1Ibsb zWwV!agMwsKp5oJ%mCs?#;iJ~Y3jva zI2rc;Wkr)fJ1{fJTveC>#E;1TzdC6)D>M3LXl5u0B;!YmGD7`7wg59U)Ax93P!nBT zeoWU)orGdw#?&mrpFGB3Y9q6o|DLhxULu#YcK^5f1mN$9nacheu(SykKpqy;022z| z?VW1Pj*mV4JH6_BoSS;Te_Q8WTGh$R64es=PR&8XTKS#Ya)}2m_0W|4{v!QbKAq7h zm%M={dp1nJ+F+9sn=DJNeEKbQhvC@B^9MT1-tW|^YrN7!A2ApuHV zKmJjjID*i~Yx}z|`jX&Gm%PpT#Qyyh=kkya=SjY*plby1Kl9p!yf$+3e=7TgU-+t* z%F-G!8JCvHGl&3r1TNns-!p9Hls{KKaAxE)Y-GC2+Y`LGdV`1fx?ykiO|N?16KkdD zG^FzpEydbWgAA;AohWxgBRS=nbIf$hiVTxjQD}>864m#cia{e{7#dPHX73(5|I5>E z%%04ZP{OfL-_$YO`Ypn(2X0$cy>&d(nJ3|fTON<;5w!lFizeT63qMneS6vg;2s`v; z;aa?s$CF_poCxit&CnWda6Qt-w=o-mRNj2DijTE8soaNbAZjL;Y-hs zYY!H`xKOqR$Wocf+`vX=FoR3S_8Jn_y1nL$Ryp3{_-`EhyEr*%saepZL7#aQ+x>!y z>?;tEx092)eaKe9If9Fj!?rZm#qU)4!?sZ8VhEx|z_Z25)F$WZ2<9F|z(dC)hY>VF zF!P8ljrw-jme$#bavBvXRZw>n>i^lQCq82LPW$D=pZa?`Jx1bAbbq*haKu*Ge}api zyry5574$C<{Z@;i^cNOv)GtSEA^rrQtoTe$3RVqeLh}VkhNs z!sb;VFF%r?XVHj}^^bfW^mPMH8OEd|D~e4vV)K5NQ*7tDS&C|&(*1;3{F6AT!pF$G zJV~6ip*&N~kToaLj3}o$)|QUtQ0X8lEl`oEe1}Sh7DmOscQ;h1i$)~UIHRI=itf6k zy6*GFU2AwWv5`UM>+fcW&f;&e<2Hw*e@j%ywKV zTFzAkBysTxZ-Ry-^uUp0kJ47HALMX55Su`3UL0JR+cQHVdGR%j-7(U4QMF0plGD1- zRZUCc^1^C$Q?oo>eC)~H^xEg`Qp>-x|A2#5C)%g>5ulfwicRX`6OkSb>20d^ZN2Tv z>b1-9(r7_B44YYlx0YD^)ol?r@;L*6g}W)YWX!vYx~a;pgi;Yr8G)ux&9tM^gumq5 z{`|ZONRn#0sk&&|o1h_GMeQMhJqxUVw+{_%xe{T~AN2azfA;n!HJRSIgPYo$%q27; z20x^Ig|=S%`q1$f&9zXpj2nwhDr`Dc{If>G68)uD=JQ}_1@qig+epG+kA@WTwLsrg z$qKv4U@UC`m4j}oVRF)9z8!QGO>Z>2)!4aoQsU#1AL19Oe(k1Kq8X7QrIYs1snfO^ z%T`)bzC*cXGCK8Svfer)wMy%XL(`)N zo@#~^$tzF2;PX$;Juz@kDhj@@B!fsA8rE%IYJ;mwMQaOhWpi_xX1uE8h{8*6^~??X z2p<)hl8}t7RBcnbq$<+GNAEhibZnVheh)jVYs^1&a}JmHm}T8ZZY5Bj(9Bh%{D(UwzE-0^*Go!)mwys9h@tBfg7Uu^j9Hs;;#I&T*b546`uH-zSqg9c8J?(G{pCa^p{GmY*X#HLt`N}S+N;2=2e0G_qvvJ*c{2I zQsg6MuhGbXM$rX_?zfFPn9ZT#pGg%(Bcg03-S>5U@^2b=eQgtmMq6xTKFlRl4WG!nQq{d& zQYC8`;UqtC6!ShpjVA{2T_G^Fnu(PZN4 zZI=#Srx=Xs+OPRkswn)Jk49=Vj;5$|ruO>h*|kkf?b*rae5zs8PyG-t=zcAO^Q(zb zv?ZP}ei6-F1@-t8ejU5|kbAi7`tWv*WI@M*D!3?xz84L7E|4Qn*)JdJc)rp!w4VA( zA>D1+g>6?}NS`P%HsRW`&vLJjib?I_l|DbGQlcdD#HYpT)C{lUSpX#_Fw&hTk5p@l zk{sViy$;zw#3Wz0b9p~BWV%i>&Kfzn7+= zpD;6tNK<%nRzl5ISe?l0;%y7FbS$jC_~6^{!utH=MaL>0}!eWxO-wR{TRzlaJ- zk2wlGxu04@ZSixd7_p>?K2!7=bYSX^noDjw@@OYEQja{(j4ZKZ(P7z^pq@!hJ5WSL z+KKHsG-T0xF%7edm}Ykz8s>-QHwZNsWs=wOtcVK8L%1Iazc_uxX}gQRJyDb%c*f|` z*bz;r^wMT!qkg`{M$+&i zMVeMEPJOE6hzB3@*O%}06R3`AZ#rgsddY{HU;h6nN#o1?h=l8me=&WBKHiu4WPn~* zwNnLdLl^h7^?7;q`4|EisaYloj9-);pwNbB5KK7INDfRV0$(_eHjww>v zk*#JH&?$|~NEVri$x}wfavxE&jM4v>?$`Cf1#5aEzErEq*hq^Dni81(QLzlH`OOyB zw2X4gM0a(rh$=3oUhLW~YR@uiY$iPz8iTir&EyghaSETLYngMXx%<;CHd&AugX$H- z=3Binzdt@z`n+Mo5MdUEc~pjZk13tIeRw%N#VsB;8PmViUFp<~C5(|LKT;Z}GH8|7 z%n>6e>_^J$IWWm-g>&XxwH|2c)cPDV%!GV2%gfX=%B^z+W#@Dc6`GYAW^~mND~jpM z{j>JiE5EGUyTAyA%3F#WBQw8wZc{dQ8MW2i7NZ2rZkCuzX4)d+4|S?lHkZ6VMhl}S zw>B%S(lL4@blL|V8xj8x=Ly!_yg$mqXh#1<(`X8D26D5~#>p$KNg8>TkhR%y{YO*q zwuM_v2j);|_H5=5X*P8&^Sf42qh;JQmGlwJ$GW?Ux`Nz(nnO9=$@(lGIu0$Ds8%9J zq1>*b+;ig6tSqJvdfW4{8b19OAO45S_;E_8w5T9v*k;nrB(BUw{feKjW_vNZWEGq{ zj*^j^riXgH4z)&BR{`ZnteNSiy-|^W3=pQ@M!ozm<9VD|X>BfEX`ADPS;5T)WagO> zP=XaL%&fsb`e-w1X_!gop-VD-G?QZ5n=_}PWzMnOpKEEy?8Nun zoQ5SJw|8W!C@s9?zApu$4jWI`v;o!3r+-y*GgLDtI?+kUw4=?mZGqz&?buCCm1j8& z^=6>v+)(#2-dy^H6t7W#>;-<;qZ8u1X4iuTgRkP3_P{9k$I5}R=m|=m1lX!YnGfA= z6xyOS4ebmJebUFrZ#DIWK+ZJnC)E9=hdf@AkuJ_SW=1Vu=_i=z*&Ml2o{sZdSe!bG zC)#ZOKeEU(k?bdsAUO%?zf77Eo=ljN+W!?9d*?d(;I%O#aPOTrJxdzNq!-oKSw0bc z(U852WRZpY{p~b%iKd~QL6kryGQ7Phq>iO#4^Q7<_i|zRG`)_B@TBUpjBO=m{N>b2 zf{~d;WR}Gt?#0K>)q>HMQ(omPq9x31#c5ZC-?EWbDjh-7U^O&;#vikk+t6d*+wQH}}NT1>r`~M0% z(x4{Js0n!wg2+v^5JvE#w6kD;fG8e}t@T13KwGQU8j=k}mLzUAl89rAb;JX?s=n5$ zt$3k`)LQ+TPFc$k@rUA(&@?YI?vVd7uUFK>PN356&<_IbYx## zmFH_dSa`M~XQCa;wq$S$xsEo>Gn04Mk!U$hmCB9PqcJ*HHTcX23Aq% z8c^urYDlE6N6%n zHm6l^DjrF6I$aKjT~?d|-DrR$ib%!9bg&;@pu2COo~=#At-NH`7qi%~$ygamqn|(+ z`}8q5fE?)b@KTs!cNG;2CcC}FDze5G@J$V6_QP3pXfY>#<%`%w zZMWeBbH`&I(!0OFH!Ss6*r{b}yP!0LlA17>RnNhrdeYtEnxAIDZM_-P16J&z7SULWlRDp62ht^Ayv9 zQQ?wN@iV#5D7omof{7}Mv2?8#GiYutR#06Xj^UH~Af7pzF+)SwYcZOJ)pFzWn$y<3 zIEgee4x=|@?3XM$C96#lO6_KsBnmFYDmncJ6v=j%qtL@{$XL%ijmmHY?JC0or2m=g zRaG0M)^H>2UxTCQH&sd5b(lwOb({{q8_sU;#q~jS?h*GLsS^Y?;uZ|nsD2qn)nDLI TR=XdUYT1(|5Uru;gV^{t={w=O diff --git a/package.json b/package.json index 4612a22c..61958b92 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@playwright/test": "^1.58.2", "@testing-library/jest-dom": "^6.9.1", "@types/cli-progress": "^3.11.6", + "@types/dompurify": "^3.2.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20", "@types/react": "^19", diff --git a/src/app/admin/(dashboard)/_state/landing-page/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts index d4abaf81..152f16d7 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/profile.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts @@ -55,10 +55,15 @@ const programInovasi = proxy({ programInovasi.findMany.load(); return toast.success("Sukses menambahkan"); } - console.log(res); + if (process.env.NODE_ENV === 'development') { + console.log(res); + } return toast.error("failed create"); } catch (error) { - console.log((error as Error).message); + if (process.env.NODE_ENV === 'development') { + console.error("Create error:", error); + } + toast.error("Gagal menambahkan data"); } finally { programInovasi.create.loading = false; } @@ -91,13 +96,17 @@ const programInovasi = proxy({ programInovasi.findMany.total = res.data.total || 0; programInovasi.findMany.totalPages = res.data.totalPages || 1; } else { - console.error("Failed to load pegawai:", res.data?.message); + if (process.env.NODE_ENV === 'development') { + console.error("Failed to load pegawai:", res.data?.message); + } programInovasi.findMany.data = []; programInovasi.findMany.total = 0; programInovasi.findMany.totalPages = 1; } } catch (error) { - console.error("Error loading pegawai:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading pegawai:", error); + } programInovasi.findMany.data = []; programInovasi.findMany.total = 0; programInovasi.findMany.totalPages = 1; @@ -112,19 +121,25 @@ const programInovasi = proxy({ image: true; }; }> | null, + loading: false, async load(id: string) { try { - const res = await fetch(`/api/landingpage/programinovasi/${id}`); - if (res.ok) { - const data = await res.json(); - programInovasi.findUnique.data = data.data ?? null; + programInovasi.findUnique.loading = true; + const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get(); + if (res.data?.success) { + programInovasi.findUnique.data = res.data.data ?? null; + return res.data.data; } else { - console.error("Failed to fetch program inovasi:", res.statusText); + toast.error(res.data?.message || "Gagal memuat data program inovasi"); programInovasi.findUnique.data = null; + return null; } } catch (error) { console.error("Error fetching program inovasi:", error); programInovasi.findUnique.data = null; + return null; + } finally { + programInovasi.findUnique.loading = false; } }, }, @@ -135,27 +150,18 @@ const programInovasi = proxy({ try { programInovasi.delete.loading = true; + const res = await (ApiFetch.api.landingpage.programinovasi as any)["del"][id].delete(); - const response = await fetch( - `/api/landingpage/programinovasi/del/${id}`, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ); - - const result = await response.json(); - - if (response.ok && result?.success) { - toast.success(result.message || "Program inovasi berhasil dihapus"); - await programInovasi.findMany.load(); // refresh list + if (res.data?.success) { + toast.success(res.data.message || "Program inovasi berhasil dihapus"); + await programInovasi.findMany.load(); } else { - toast.error(result?.message || "Gagal menghapus program inovasi"); + toast.error(res.data?.message || "Gagal menghapus program inovasi"); } } catch (error) { - console.error("Gagal delete:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Gagal delete:", error); + } toast.error("Terjadi kesalahan saat menghapus program inovasi"); } finally { programInovasi.delete.loading = false; @@ -174,20 +180,11 @@ const programInovasi = proxy({ } try { - const response = await fetch(`/api/landingpage/programinovasi/${id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + programInovasi.update.loading = true; + const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get(); - const result = await response.json(); - - if (result?.success) { - const data = result.data; + if (res.data?.success) { + const data = res.data.data; this.id = data.id; this.form = { name: data.name, @@ -197,13 +194,15 @@ const programInovasi = proxy({ }; return data; } else { - throw new Error( - result?.message || "Gagal mengambil data program inovasi" - ); + toast.error(res.data?.message || "Gagal mengambil data program inovasi"); + return null; } } catch (error) { - console.error((error as Error).message); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading program inovasi:", error); + } toast.error("Terjadi kesalahan saat mengambil data program inovasi"); + return null; } finally { programInovasi.update.loading = false; } @@ -221,41 +220,25 @@ const programInovasi = proxy({ try { programInovasi.update.loading = true; + const res = await (ApiFetch.api.landingpage.programinovasi as any)[this.id].put({ + name: this.form.name, + description: this.form.description, + imageId: this.form.imageId, + link: this.form.link, + }); - const response = await fetch( - `/api/landingpage/programinovasi/${this.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: this.form.name, - description: this.form.description, - imageId: this.form.imageId, - link: this.form.link, - }), - } - ); - - 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) { + if (res.data?.success) { toast.success("Berhasil update program inovasi"); - await programInovasi.findMany.load(); // refresh list + await programInovasi.findMany.load(); return true; } else { - throw new Error(result.message || "Gagal update program inovasi"); + toast.error(res.data?.message || "Gagal update program inovasi"); + return false; } } catch (error) { - console.error("Error updating program inovasi:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error updating program inovasi:", error); + } toast.error( error instanceof Error ? error.message @@ -443,7 +426,7 @@ const pejabatDesa = proxy({ const templateMediaSosial = z.object({ name: z.string().min(3, "Nama minimal 3 karakter"), imageId: z.string().nullable().optional(), - iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"), + iconUrl: z.string().optional(), // ✅ Optional - tidak selalu required icon: z.string().nullable().optional(), }); @@ -484,10 +467,15 @@ const mediaSosial = proxy({ mediaSosial.findMany.load(); return toast.success("Sukses menambahkan"); } - console.log(res); + if (process.env.NODE_ENV === 'development') { + console.log(res); + } return toast.error("failed create"); } catch (error) { - console.log((error as Error).message); + if (process.env.NODE_ENV === 'development') { + console.log((error as Error).message); + } + toast.error("Gagal menambahkan data"); } finally { mediaSosial.create.loading = false; } @@ -518,13 +506,17 @@ const mediaSosial = proxy({ mediaSosial.findMany.total = res.data.total || 0; mediaSosial.findMany.totalPages = res.data.totalPages || 1; } else { - console.error("Failed to load media sosial:", res.data?.message); + if (process.env.NODE_ENV === 'development') { + console.error("Failed to load media sosial:", res.data?.message); + } mediaSosial.findMany.data = []; mediaSosial.findMany.total = 0; mediaSosial.findMany.totalPages = 1; } } catch (error) { - console.error("Error loading media sosial:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading media sosial:", error); + } mediaSosial.findMany.data = []; mediaSosial.findMany.total = 0; mediaSosial.findMany.totalPages = 1; @@ -539,25 +531,32 @@ const mediaSosial = proxy({ image: true; }; }> | null, + loading: false, async load(id: string) { if (!id) { toast.warn("ID tidak valid"); return null; } - mediaSosial.update.loading = true; + mediaSosial.findUnique.loading = true; try { - const res = await fetch(`/api/landingpage/mediasosial/${id}`); - if (res.ok) { - const data = await res.json(); - mediaSosial.findUnique.data = data.data ?? null; + const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get(); + if (res.data?.success) { + mediaSosial.findUnique.data = res.data.data ?? null; + return res.data.data; } else { - console.error("Failed to fetch media sosial:", res.statusText); + toast.error(res.data?.message || "Gagal memuat data media sosial"); mediaSosial.findUnique.data = null; + return null; } } catch (error) { - console.error("Error fetching media sosial:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error fetching media sosial:", error); + } mediaSosial.findUnique.data = null; + return null; + } finally { + mediaSosial.findUnique.loading = false; } }, }, @@ -568,24 +567,18 @@ const mediaSosial = proxy({ try { mediaSosial.delete.loading = true; + const res = await (ApiFetch.api.landingpage.mediasosial as any)["del"][id].delete(); - const response = await fetch(`/api/landingpage/mediasosial/del/${id}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }); - - const result = await response.json(); - - if (response.ok && result?.success) { - toast.success(result.message || "Media Sosial berhasil dihapus"); - await mediaSosial.findMany.load(); // refresh list + if (res.data?.success) { + toast.success(res.data.message || "Media Sosial berhasil dihapus"); + await mediaSosial.findMany.load(); } else { - toast.error(result?.message || "Gagal menghapus media sosial"); + toast.error(res.data?.message || "Gagal menghapus media sosial"); } } catch (error) { - console.error("Gagal delete:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Gagal delete:", error); + } toast.error("Terjadi kesalahan saat menghapus media sosial"); } finally { mediaSosial.delete.loading = false; @@ -603,43 +596,32 @@ const mediaSosial = proxy({ return null; } - mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal - + mediaSosial.update.loading = true; try { - const response = await fetch(`/api/landingpage/mediasosial/${id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get(); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - - if (result?.success) { - const data = result.data; + if (res.data?.success) { + const data = res.data.data; this.id = data.id; this.form = { name: data.name || "", imageId: data.imageId || null, iconUrl: data.iconUrl || "", icon: data.icon || null, - }; return data; } else { - throw new Error( - result?.message || "Gagal mengambil data media sosial" - ); + toast.error(res.data?.message || "Gagal mengambil data media sosial"); + return null; } } catch (error) { - console.error((error as Error).message); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading media sosial:", error); + } toast.error("Terjadi kesalahan saat mengambil data media sosial"); + return null; } finally { - mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error + mediaSosial.update.loading = false; } }, @@ -655,41 +637,25 @@ const mediaSosial = proxy({ try { mediaSosial.update.loading = true; + const res = await (ApiFetch.api.landingpage.mediasosial as any)[this.id].put({ + name: this.form.name, + imageId: this.form.imageId, + iconUrl: this.form.iconUrl, + icon: this.form.icon, + }); - const response = await fetch( - `/api/landingpage/mediasosial/${this.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: this.form.name, - imageId: this.form.imageId, - iconUrl: this.form.iconUrl, - icon: this.form.icon, - }), - } - ); - - 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) { + if (res.data?.success) { toast.success("Berhasil update media sosial"); - await mediaSosial.findMany.load(); // refresh list + await mediaSosial.findMany.load(); return true; } else { - throw new Error(result.message || "Gagal update media sosial"); + toast.error(res.data?.message || "Gagal update media sosial"); + return false; } } catch (error) { - console.error("Error updating media sosial:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error updating media sosial:", error); + } toast.error( error instanceof Error ? error.message diff --git a/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx index c2e26cf6..e0a7cf84 100644 --- a/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx @@ -8,6 +8,7 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; +import DOMPurify from 'dompurify'; function DetailProgramInovasi() { const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) @@ -85,7 +86,7 @@ function DetailProgramInovasi() { Deskripsi - + diff --git a/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx index b2c30db0..0c68b120 100644 --- a/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx @@ -6,6 +6,7 @@ import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; +import DOMPurify from 'dompurify'; import HeaderSearch from '../../../_com/header'; import profileLandingPageState from '../../../_state/landing-page/profile'; @@ -90,7 +91,7 @@ function ListProgramInovasi({ search }: { search: string }) { {item.name} - + @@ -144,7 +145,7 @@ function ListProgramInovasi({ search }: { search: string }) { {/* Description */} Deskripsi - + {/* Link */} -- 2.49.1 From b86a3a85c31a9ac91acd4401b9085c77ad63b527 Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 25 Feb 2026 10:45:27 +0800 Subject: [PATCH 94/97] fix: force default light mode for public pages and admin - Set defaultColorScheme='light' in root MantineProvider - Change darkModeStore default from system preference to false (light) - Add MantineProvider with light theme to darmasaba/layout.tsx - Remove dark mode dependency from ModuleView component - Prevent system color scheme from affecting initial page load This ensures consistent light mode on first visit for both public pages and admin panel, regardless of OS settings. Co-authored-by: Qwen-Coder --- .../main-page/landing-page/ModuleView.tsx | 7 ++-- src/app/darmasaba/layout.tsx | 35 +++++++++++-------- src/app/layout.tsx | 12 +++---- src/state/darkModeStore.ts | 11 +++--- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx b/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx index 695f0572..f13710e5 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx @@ -10,8 +10,7 @@ import { SimpleGrid, Skeleton, Stack, - Text, - useMantineColorScheme + Text } from "@mantine/core"; import { useShallowEffect } from "@mantine/hooks"; import { Prisma } from "@prisma/client"; @@ -24,8 +23,6 @@ type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: tr function ModuleItem({ data }: { data: ProgramInovasiItem }) { const router = useTransitionRouter(); - const { colorScheme } = useMantineColorScheme(); - const isDark = colorScheme === "dark"; return ( @@ -37,7 +34,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) { role="button" tabIndex={0} className="cursor-pointer transition-all" - bg={isDark ? "dark.6" : "white"} + bg="white" >
{data.image?.link ? ( diff --git a/src/app/darmasaba/layout.tsx b/src/app/darmasaba/layout.tsx index afe980de..71971662 100644 --- a/src/app/darmasaba/layout.tsx +++ b/src/app/darmasaba/layout.tsx @@ -1,25 +1,32 @@ +"use client"; + import colors from "@/con/colors"; +import { MantineProvider, createTheme } from "@mantine/core"; import { Box, Space, Stack } from "@mantine/core"; import { Navbar } from "@/app/darmasaba/_com/Navbar"; import Footer from "./_com/Footer"; - +const theme = createTheme({ + defaultColorScheme: "light", +}); export default function Layout({ children }: { children: React.ReactNode }) { return ( - - - - - {children} - -