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 aef74337..c1f0eda6 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 892c4639..4612a22c 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "colors": "^1.4.0", "date-fns": "^4.1.0", "dayjs": "^1.11.13", + "dompurify": "^3.3.1", "dotenv": "^17.2.3", "elysia": "^1.3.5", "embla-carousel": "^8.6.0", diff --git a/prisma/seed.ts b/prisma/seed.ts index 3c645b5b..1a3b7b93 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -69,8 +69,8 @@ import { seedProfilPpd } from "./_seeder_list/ppid/profil-ppid/seed_profil_ppd"; (async () => { // 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() {