From f90477ed6359b57dcf1b5f76e62f72b00609fe59 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 5 Mar 2026 11:20:45 +0800 Subject: [PATCH] fix(apbdes-edit): preserve realisasi data when editing APBDes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix backend updt.ts to preserve realisasiItems from old items - Load existing items with realisasiItems before delete - Re-create realisasiItems for new items based on kode match - Recalculate totalRealisasi, selisih, persentase after restore - Update frontend state to handle realisasi fields - Add realisasi, selisih, persentase to ApbdesItemSchema - Fix edit.load() to map totalRealisasi → realisasi - Fix edit.update() to omit calculated fields when sending to backend - Update edit page.tsx to display realisasi data - Fix load data to use item.totalRealisasi (not item.realisasi) - Add Realisasi, Selisih, % columns to items table - Update handleAddItem and handleReset to preserve realisasi fields Root cause: Backend was resetting totalRealisasi=0 for all items on update, and frontend was accessing wrong field name (realisasi vs totalRealisasi) Co-authored-by: Qwen-Coder --- .../(dashboard)/_state/landing-page/apbdes.ts | 33 +++++- .../landing-page/apbdes/[id]/edit/page.tsx | 31 +++++- .../_lib/landing_page/apbdes/updt.ts | 100 ++++++++++++++++-- .../main-page/apbdes/lib/grafikRealisasi.tsx | 14 +-- 4 files changed, 152 insertions(+), 26 deletions(-) diff --git a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts index da26a972..46141de3 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts @@ -5,13 +5,17 @@ import { toast } from "react-toastify"; import { proxy } from "valtio"; import { z } from "zod"; -// --- Zod Schema untuk APBDes Item (tanpa field kalkulasi) --- +// --- Zod Schema untuk APBDes Item (dengan field kalkulasi) --- const ApbdesItemSchema = z.object({ kode: z.string().min(1, "Kode wajib diisi"), uraian: z.string().min(1, "Uraian wajib diisi"), anggaran: z.number().min(0, "Anggaran tidak boleh negatif"), level: z.number().int().min(1).max(3), tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(), + // Field kalkulasi dari realisasiItems (auto-calculated di backend) + realisasi: z.number().min(0).default(0), + selisih: z.number().default(0), + persentase: z.number().default(0), }); const ApbdesFormSchema = z.object({ @@ -35,7 +39,7 @@ const defaultApbdesForm = { items: [] as z.infer[], }; -// --- Helper: Normalize item (tanpa kalkulasi, backend yang hitung) --- +// --- Helper: Normalize item (dengan field kalkulasi) --- function normalizeItem(item: Partial>): z.infer { return { kode: item.kode || "", @@ -43,6 +47,9 @@ function normalizeItem(item: Partial>): z.infer anggaran: item.anggaran ?? 0, level: item.level || 1, tipe: item.tipe ?? null, + realisasi: item.realisasi ?? 0, + selisih: item.selisih ?? 0, + persentase: item.persentase ?? 0, }; } @@ -248,6 +255,9 @@ const apbdes = proxy({ kode: item.kode, uraian: item.uraian, anggaran: item.anggaran, + realisasi: item.totalRealisasi || 0, + selisih: item.selisih || 0, + persentase: item.persentase || 0, level: item.level, tipe: item.tipe || 'pendapatan', })), @@ -275,11 +285,24 @@ const apbdes = proxy({ try { this.loading = true; // Include the ID in the request body + // Omit realisasi, selisih, persentase karena itu calculated fields di backend const requestData = { - ...parsed.data, - id: this.id, // Add the ID to the request body + tahun: parsed.data.tahun, + name: parsed.data.name, + deskripsi: parsed.data.deskripsi, + jumlah: parsed.data.jumlah, + imageId: parsed.data.imageId, + fileId: parsed.data.fileId, + id: this.id, + items: parsed.data.items.map(item => ({ + kode: item.kode, + uraian: item.uraian, + anggaran: item.anggaran, + level: item.level, + tipe: item.tipe ?? null, + })), }; - + const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData); if (res.data?.success) { 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 31fb2faf..e03b7717 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 @@ -44,6 +44,9 @@ type ItemForm = { anggaran: number; level: number; tipe: 'pendapatan' | 'belanja' | 'pembiayaan'; + realisasi?: number; + selisih?: number; + persentase?: number; }; function EditAPBDes() { @@ -72,6 +75,9 @@ function EditAPBDes() { anggaran: 0, level: 1, tipe: 'pendapatan', + realisasi: 0, + selisih: 0, + persentase: 0, }); // Simpan data original untuk reset form @@ -125,9 +131,9 @@ function EditAPBDes() { kode: item.kode, uraian: item.uraian, anggaran: item.anggaran, - realisasi: item.realisasi, - selisih: item.selisih, - persentase: item.persentase, + realisasi: item.totalRealisasi || 0, + selisih: item.selisih || 0, + persentase: item.persentase || 0, level: item.level, tipe: item.tipe || 'pendapatan', })), @@ -155,7 +161,7 @@ function EditAPBDes() { }; const handleAddItem = () => { - const { kode, uraian, anggaran, level, tipe } = newItem; + const { kode, uraian, anggaran, level, tipe, realisasi, selisih, persentase } = newItem; if (!kode || !uraian) { return toast.warn('Kode dan uraian wajib diisi'); } @@ -166,6 +172,9 @@ function EditAPBDes() { kode, uraian, anggaran, + realisasi: realisasi || 0, + selisih: selisih || 0, + persentase: persentase || 0, level, tipe: finalTipe, }); @@ -176,6 +185,9 @@ function EditAPBDes() { anggaran: 0, level: 1, tipe: 'pendapatan', + realisasi: 0, + selisih: 0, + persentase: 0, }); }; @@ -264,6 +276,9 @@ function EditAPBDes() { anggaran: 0, level: 1, tipe: 'pendapatan', + realisasi: 0, + selisih: 0, + persentase: 0, }); toast.info('Form dikembalikan ke data awal'); @@ -527,6 +542,9 @@ function EditAPBDes() { Kode Uraian Anggaran + Realisasi + Selisih + % Level Tipe Aksi @@ -542,6 +560,11 @@ function EditAPBDes() { {item.uraian} {item.anggaran.toLocaleString('id-ID')} + {item.realisasi?.toLocaleString('id-ID') || '0'} + 0 ? 'red' : 'green' }}> + {item.selisih?.toLocaleString('id-ID') || '0'} + + {item.persentase?.toFixed(2) || '0'}% L{item.level} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts index bd4b4c3e..4e9dc782 100644 --- a/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts +++ b/src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts @@ -28,6 +28,16 @@ export default async function apbdesUpdate(context: Context) { // 1. Pastikan APBDes ada const existing = await prisma.aPBDes.findUnique({ where: { id }, + include: { + items: { + where: { isActive: true }, + include: { + realisasiItems: { + where: { isActive: true }, + }, + }, + }, + }, }); if (!existing) { @@ -38,17 +48,35 @@ export default async function apbdesUpdate(context: Context) { }; } - // 2. Hapus semua item lama (cascade akan menghapus realisasiItems juga) + // 2. Build map untuk preserve realisasiItems berdasarkan kode + const existingItemsMap = new Map(); + + existing.items.forEach(item => { + existingItemsMap.set(item.kode, { + id: item.id, + realisasiItems: item.realisasiItems, + }); + }); + + // 3. Hapus semua item lama (cascade akan menghapus realisasiItems juga) + // TAPI kita sudah save realisasiItems di map atas await prisma.aPBDesItem.deleteMany({ where: { apbdesId: id }, }); - // 3. Buat item baru dengan auto-calculate fields + // 4. Buat item baru dengan preserve realisasiItems await prisma.aPBDesItem.createMany({ - data: body.items.map((item) => { + data: await Promise.all(body.items.map(async (item) => { const anggaran = item.anggaran; - const totalRealisasi = 0; // Reset karena items baru - const selisih = anggaran - totalRealisasi; // Sisa anggaran (positif = belum digunakan) + + // Check apakah item ini punya realisasiItems lama + const existingItem = existingItemsMap.get(item.kode); + const realisasiItemsData = existingItem?.realisasiItems || []; + const totalRealisasi = realisasiItemsData.reduce((sum, r) => sum + r.jumlah, 0); + const selisih = anggaran - totalRealisasi; const persentase = anggaran > 0 ? (totalRealisasi / anggaran) * 100 : 0; return { @@ -63,16 +91,68 @@ export default async function apbdesUpdate(context: Context) { persentase, isActive: true, }; - }), + })), }); - // 4. Dapatkan semua item yang baru dibuat untuk mendapatkan ID-nya + // 5. Dapatkan semua item yang baru dibuat untuk mendapatkan ID-nya const allItems = await prisma.aPBDesItem.findMany({ where: { apbdesId: id }, select: { id: true, kode: true }, }); - // 5. Update parentId untuk setiap item + // 6. Build map baru untuk item IDs + const newItemIdsMap = new Map(); + allItems.forEach(item => { + newItemIdsMap.set(item.kode, item.id); + }); + + // 7. Re-create realisasiItems dengan link ke item IDs yang baru + for (const [oldKode, oldItemData] of existingItemsMap.entries()) { + if (oldItemData.realisasiItems.length > 0) { + const newItemId = newItemIdsMap.get(oldKode); + if (newItemId) { + // Re-create realisasiItems untuk item ini + await prisma.realisasiItem.createMany({ + data: oldItemData.realisasiItems.map(r => ({ + apbdesItemId: newItemId, + kode: r.kode, + jumlah: r.jumlah, + tanggal: r.tanggal, + keterangan: r.keterangan, + buktiFileId: r.buktiFileId, + isActive: true, + })), + }); + } + } + } + + // 8. Recalculate totalRealisasi setelah re-create realisasiItems + for (const [kode, _] of existingItemsMap.entries()) { + const newItemId = newItemIdsMap.get(kode); + if (newItemId) { + const realisasiItems = await prisma.realisasiItem.findMany({ + where: { apbdesItemId: newItemId, isActive: true }, + }); + const totalRealisasi = realisasiItems.reduce((sum, r) => sum + r.jumlah, 0); + + const item = await prisma.aPBDesItem.findUnique({ + where: { id: newItemId }, + }); + + if (item) { + const selisih = item.anggaran - totalRealisasi; + const persentase = item.anggaran > 0 ? (totalRealisasi / item.anggaran) * 100 : 0; + + await prisma.aPBDesItem.update({ + where: { id: newItemId }, + data: { totalRealisasi, selisih, persentase }, + }); + } + } + } + + // 9. Update parentId untuk setiap item const itemsForParentUpdate = allItems.map(item => ({ id: item.id, kode: item.kode, @@ -80,7 +160,7 @@ export default async function apbdesUpdate(context: Context) { await assignParentIdsToApbdesItems(itemsForParentUpdate); - // 6. Update data APBDes + // 10. Update data APBDes await prisma.aPBDes.update({ where: { id }, data: { @@ -93,7 +173,7 @@ export default async function apbdesUpdate(context: Context) { }, }); - // 7. Ambil data lengkap untuk response (include realisasiItems) + // 11. Ambil data lengkap untuk response (include realisasiItems) const result = await prisma.aPBDes.findUnique({ where: { id }, include: { diff --git a/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx b/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx index 4307ebef..3cd7e150 100644 --- a/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx +++ b/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx @@ -105,8 +105,14 @@ export default function GrafikRealisasi({ apbdesData }: any) { GRAFIK REALISASI APBDes {tahun} + + + + + + {/* Summary Total Keseluruhan */} - + <> TOTAL KESELURUHAN @@ -125,12 +131,6 @@ export default function GrafikRealisasi({ apbdesData }: any) { /> - - - - - - ); } \ No newline at end of file