From 2d901912ead414fd2f66da0672a64297e9c231ef Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 4 Mar 2026 11:51:58 +0800 Subject: [PATCH] fix(realisasi): add kode field to RealisasiItem and simplify table display - Add kode field to RealisasiItem model in Prisma schema - Update API endpoints (create, update) to accept kode parameter - Update state management with proper type definitions - Add kode input field in RealisasiManager component - Simplify realisasiTable to show flat list (Kode, Uraian, Realisasi, %) - Remove section grouping and expandable details - Fix race condition in findUnique.load() with loading guard - Fix linting errors across multiple files Co-authored-by: Qwen-Coder --- prisma/schema.prisma | 6 +- .../(dashboard)/_state/landing-page/apbdes.ts | 44 ++-- .../apbdes/[id]/RealisasiManager.tsx | 34 ++- .../landing-page/apbdes/[id]/page.tsx | 2 +- .../_lib/landing_page/apbdes/index.ts | 2 + .../landing_page/apbdes/realisasi/create.ts | 2 + .../landing_page/apbdes/realisasi/update.ts | 2 + .../(pages)/musik/musik-desa/page.tsx | 4 +- .../darmasaba/_com/main-page/apbdes/index.tsx | 33 ++- .../main-page/apbdes/lib/grafikRealisasi.tsx | 2 +- .../main-page/apbdes/lib/realisasiTable.tsx | 229 +++++------------- 11 files changed, 144 insertions(+), 216 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5c12fa2c..4519bad8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -238,19 +238,21 @@ model APBDesItem { // Model baru untuk multiple realisasi per item model RealisasiItem { id String @id @default(cuid()) + kode String? // Kode realisasi, mirip dengan APBDesItem apbdesItemId String apbdesItem APBDesItem @relation(fields: [apbdesItemId], references: [id], onDelete: Cascade) - + jumlah Float // Jumlah realisasi dalam Rupiah tanggal DateTime @db.Date // Tanggal realisasi keterangan String? @db.Text // Keterangan tambahan (opsional) buktiFileId String? // FileStorage ID untuk bukti/foto (opsional) - + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? isActive Boolean @default(true) + @@index([kode]) @@index([apbdesItemId]) @@index([tanggal]) } diff --git a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts index c858fba5..7e05b86e 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts @@ -14,14 +14,6 @@ const ApbdesItemSchema = z.object({ tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(), }); -// --- Zod Schema untuk Realisasi Item --- -const RealisasiItemSchema = z.object({ - jumlah: z.number().min(0, "Jumlah tidak boleh negatif"), - tanggal: z.string(), - keterangan: z.string().optional(), - buktiFileId: z.string().optional(), -}); - const ApbdesFormSchema = z.object({ tahun: z.number().int().min(2000, "Tahun tidak valid"), name: z.string().optional(), @@ -157,33 +149,37 @@ const apbdes = proxy({ findUnique: { data: null as | Prisma.APBDesGetPayload<{ - include: { image: true; file: true; items: true }; + include: { image: true; file: true; items: { include: { realisasiItems: true } } }; }> | null, loading: false, error: null as string | null, - + async load(id: string) { if (!id || id.trim() === '') { this.data = null; this.error = "ID tidak valid"; return; } - + + // Prevent multiple simultaneous loads + if (this.loading) { + console.log("⚠️ Already loading, skipping..."); + return; + } + this.loading = true; this.error = null; - + try { - // Pastikan URL-nya benar const url = `/api/landingpage/apbdes/${id}`; console.log("🌐 Fetching:", url); - - // Gunakan fetch biasa atau ApiFetch dengan cara yang benar + const response = await fetch(url); const res = await response.json(); - + console.log("📦 Response:", res); - + if (res.success && res.data) { this.data = res.data; } else { @@ -322,15 +318,16 @@ const apbdes = proxy({ // ========================================= realisasi: { // Create realisasi - async create(itemId: string, data: { jumlah: number; tanggal: string; keterangan?: string; buktiFileId?: string }) { + async create(itemId: string, data: { kode: string; jumlah: number; tanggal: string; keterangan?: string; buktiFileId?: string }) { try { const res = await (ApiFetch.api.landingpage.apbdes as any)[itemId].realisasi.post(data); if (res.data?.success) { toast.success("Realisasi berhasil ditambahkan"); // Reload findUnique untuk update data - if (apbdes.findUnique.data) { - await apbdes.findUnique.load(apbdes.findUnique.data.id); + const currentId = apbdes.findUnique.data?.id; + if (currentId) { + await apbdes.findUnique.load(currentId); } return true; } else { @@ -345,15 +342,16 @@ const apbdes = proxy({ }, // Update realisasi - async update(realisasiId: string, data: { jumlah?: number; tanggal?: string; keterangan?: string; buktiFileId?: string }) { + async update(realisasiId: string, data: { kode?: string; jumlah?: number; tanggal?: string; keterangan?: string; buktiFileId?: string }) { try { const res = await (ApiFetch.api.landingpage.apbdes as any).realisasi[realisasiId].put(data); if (res.data?.success) { toast.success("Realisasi berhasil diperbarui"); // Reload findUnique untuk update data - if (apbdes.findUnique.data) { - await apbdes.findUnique.load(apbdes.findUnique.data.id); + const currentId = apbdes.findUnique.data?.id; + if (currentId) { + await apbdes.findUnique.load(currentId); } return true; } else { diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/RealisasiManager.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/RealisasiManager.tsx index 07c84f1a..26602c1d 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/RealisasiManager.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/RealisasiManager.tsx @@ -3,6 +3,8 @@ import { useProxy } from 'valtio/utils'; import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { Box, Button, @@ -23,7 +25,6 @@ import { Badge, Modal, Divider, - Loader, Center, } from '@mantine/core'; import { @@ -33,9 +34,6 @@ import { IconCalendar, IconCoin, } from '@tabler/icons-react'; -import { useState } from 'react'; -import { toast } from 'react-toastify'; -import colors from '@/con/colors'; interface RealisasiManagerProps { itemId: string; @@ -63,6 +61,7 @@ export default function RealisasiManager({ // Form state const [formData, setFormData] = useState({ + kode: '', jumlah: 0, tanggal: new Date().toISOString().split('T')[0], // YYYY-MM-DD format for input keterangan: '', @@ -70,6 +69,7 @@ export default function RealisasiManager({ const resetForm = () => { setFormData({ + kode: '', jumlah: 0, tanggal: new Date().toISOString().split('T')[0], keterangan: '', @@ -85,8 +85,9 @@ export default function RealisasiManager({ const handleOpenEdit = (realisasi: any) => { const tanggal = new Date(realisasi.tanggal); const tanggalStr = tanggal.toISOString().split('T')[0]; // YYYY-MM-DD - + setFormData({ + kode: realisasi.kode || '', jumlah: realisasi.jumlah, tanggal: tanggalStr, keterangan: realisasi.keterangan || '', @@ -100,12 +101,17 @@ export default function RealisasiManager({ return toast.warn('Jumlah realisasi harus lebih dari 0'); } + if (!formData.kode || formData.kode.trim() === '') { + return toast.warn('Kode realisasi wajib diisi'); + } + try { setLoading(true); if (editingId) { // Update existing realisasi const success = await state.realisasi.update(editingId, { + kode: formData.kode, jumlah: formData.jumlah, tanggal: new Date(formData.tanggal).toISOString(), keterangan: formData.keterangan, @@ -117,6 +123,7 @@ export default function RealisasiManager({ } else { // Create new realisasi const success = await state.realisasi.create(itemId, { + kode: formData.kode, jumlah: formData.jumlah, tanggal: new Date(formData.tanggal).toISOString(), keterangan: formData.keterangan, @@ -257,6 +264,7 @@ export default function RealisasiManager({ + Kode Tanggal Uraian Jumlah @@ -266,6 +274,11 @@ export default function RealisasiManager({ {realisasiItems.map((realisasi) => ( + + + {realisasi.kode || '-'} + + @@ -314,7 +327,7 @@ export default function RealisasiManager({ Belum ada realisasi untuk item ini - Klik tombol "Tambah Realisasi" untuk menambahkan + Klik tombol "Tambah Realisasi" untuk menambahkan @@ -349,6 +362,15 @@ export default function RealisasiManager({ + setFormData({ ...formData, kode: e.target.value })} + description="Kode unik untuk realisasi ini" + required + /> + {/* Realisasi Manager untuk setiap item */} - {data.items.map((item: any) => ( + {data.items.map((item) => ( { } // Jika bukan lagu baru, jangan reset currentTime (biar seek tidak kembali ke 0) } - }, [currentSong?.id]); // Intentional: hanya depend on song ID, bukan isPlaying + }, [currentSong?.id]); // eslint-disable-line react-hooks/exhaustive-deps -- Intentional: hanya depend on song ID, bukan isPlaying // Sync duration dari audio element jika berbeda signifikan (> 1 detik) useEffect(() => { @@ -155,7 +155,7 @@ const MusicPlayer = () => { audio.addEventListener('loadedmetadata', handleLoadedMetadata); return () => audio.removeEventListener('loadedmetadata', handleLoadedMetadata); - }, [currentSong?.id]); // Intentional: hanya depend on song ID + }, [currentSong?.id]); // eslint-disable-line react-hooks/exhaustive-deps -- Intentional: hanya depend on song ID const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); diff --git a/src/app/darmasaba/_com/main-page/apbdes/index.tsx b/src/app/darmasaba/_com/main-page/apbdes/index.tsx index 80caaca8..bf0f9748 100644 --- a/src/app/darmasaba/_com/main-page/apbdes/index.tsx +++ b/src/app/darmasaba/_com/main-page/apbdes/index.tsx @@ -4,29 +4,25 @@ import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes' import colors from '@/con/colors' import { - ActionIcon, Box, Button, - Center, + Divider, Group, - Loader, Select, SimpleGrid, Stack, Text, - Title, + Title } from '@mantine/core' -import { IconDownload } from '@tabler/icons-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { useProxy } from 'valtio/utils' +import GrafikRealisasi from './lib/grafikRealisasi' import PaguTable from './lib/paguTable' import RealisasiTable from './lib/realisasiTable' -import GrafikRealisasi from './lib/grafikRealisasi' function Apbdes() { const state = useProxy(apbdes) - const [loading, setLoading] = useState(false) const [selectedYear, setSelectedYear] = useState(null) const textHeading = { @@ -37,12 +33,9 @@ function Apbdes() { useEffect(() => { const loadData = async () => { try { - setLoading(true) await state.findMany.load() } catch (error) { console.error('Error loading data:', error) - } finally { - setLoading(false) } } loadData() @@ -73,10 +66,12 @@ function Apbdes() { ? dataAPBDes.find((item: any) => item?.tahun?.toString() === selectedYear) || dataAPBDes[0] : null - const data = (state.findMany.data || []).slice(0, 3) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const previewData = (state.findMany.data || []).slice(0, 3) return ( + {/* 📌 HEADING */} @@ -116,7 +111,7 @@ function Apbdes() { {/* COMBOBOX */} - +
+ + + Uraian + Realisasi (Rp) + % + + + + {allRealisasiRows.map(({ realisasi, parentItem }) => { + const persentase = parentItem.anggaran > 0 + ? (realisasi.jumlah / parentItem.anggaran) * 100 + : 0; -
- - - Uraian - Total Realisasi (Rp) - % - - - -
-
-
- -
+ return ( + + + {realisasi.kode} - {realisasi.keterangan} + + + + {formatRupiah(realisasi.jumlah)} + + + + = 100 + ? 'teal' + : persentase >= 60 + ? 'yellow' + : 'red' + } + > + {persentase.toFixed(2)}% + + + + ); + })} + + + )} ); } \ No newline at end of file