feat(api): implement multiple realisasi for APBDes
Schema Changes: - Add RealisasiItem model for multiple realizations per APBDesItem - Replace realisasi field with totalRealisasi (auto-calculated sum) - Add selisih and persentase as auto-calculated fields - Cascade delete realisasi when item is deleted API Changes: - Update index.ts: Add realisasi CRUD endpoints - POST /:itemId/realisasi - Create realisasi - PUT /realisasi/:realisasiId - Update realisasi - DELETE /realisasi/:realisasiId - Delete realisasi - Update create.ts: Auto-calculate totalRealisasi=0, selisih, persentase - Update updt.ts: Reset calculations when items updated - Update findUnique.ts: Include realisasiItems in response - Update findMany.ts: Include realisasiItems in response - Remove realisasi field from ApbdesItemSchema New Files: - realisasi/create.ts - Create realisasi with auto-calculation - realisasi/update.ts - Update realisasi with recalculation - realisasi/delete.ts - Soft delete with recalculation Features: - Auto-calculate totalRealisasi from sum of all realisasiItems - Auto-calculate selisih = totalRealisasi - anggaran - Auto-calculate persentase = (totalRealisasi / anggaran) * 100 - Support for bukti file attachment - Support for keterangan (notes) per realisasi - Soft delete support for audit trail UI Updates: - Update admin detail page to use totalRealisasi instead of realisasi - Update landing page realisasiTable to use totalRealisasi Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -209,16 +209,22 @@ model APBDesItem {
|
|||||||
kode String // contoh: "4", "4.1", "4.1.2"
|
kode String // contoh: "4", "4.1", "4.1.2"
|
||||||
uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
|
uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
|
||||||
anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
|
anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
|
||||||
realisasi Float
|
tipe String? // "pendapatan" | "belanja" | "pembiayaan" | null
|
||||||
selisih Float // realisasi - anggaran
|
|
||||||
persentase Float
|
|
||||||
tipe String? // (realisasi / anggaran) * 100
|
|
||||||
level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
|
level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
|
||||||
parentId String? // untuk relasi hierarki
|
parentId String? // untuk relasi hierarki
|
||||||
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
|
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
|
||||||
children APBDesItem[] @relation("APBDesItemParent")
|
children APBDesItem[] @relation("APBDesItemParent")
|
||||||
apbdesId String
|
apbdesId String
|
||||||
apbdes APBDes @relation(fields: [apbdesId], references: [id])
|
apbdes APBDes @relation(fields: [apbdesId], references: [id])
|
||||||
|
|
||||||
|
// Field kalkulasi (auto-calculated dari realisasi items)
|
||||||
|
totalRealisasi Float @default(0) // Sum dari semua realisasi
|
||||||
|
selisih Float @default(0) // totalRealisasi - anggaran
|
||||||
|
persentase Float @default(0) // (totalRealisasi / anggaran) * 100
|
||||||
|
|
||||||
|
// Relasi ke realisasi items
|
||||||
|
realisasiItems RealisasiItem[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
@@ -229,6 +235,26 @@ model APBDesItem {
|
|||||||
@@index([apbdesId])
|
@@index([apbdesId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model baru untuk multiple realisasi per item
|
||||||
|
model RealisasiItem {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
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([apbdesItemId])
|
||||||
|
@@index([tanggal])
|
||||||
|
}
|
||||||
|
|
||||||
//========================================= PRESTASI DESA ========================================= //
|
//========================================= PRESTASI DESA ========================================= //
|
||||||
model PrestasiDesa {
|
model PrestasiDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ function DetailAPBDes() {
|
|||||||
</Group>
|
</Group>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>{item.anggaran.toLocaleString('id-ID')}</TableTd>
|
<TableTd>{item.anggaran.toLocaleString('id-ID')}</TableTd>
|
||||||
<TableTd>{item.realisasi.toLocaleString('id-ID')}</TableTd>
|
<TableTd>{item.totalRealisasi.toLocaleString('id-ID')}</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text c={item.selisih >= 0 ? 'green' : 'red'}>
|
<Text c={item.selisih >= 0 ? 'green' : 'red'}>
|
||||||
{item.selisih.toLocaleString('id-ID')}
|
{item.selisih.toLocaleString('id-ID')}
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ type APBDesItemInput = {
|
|||||||
kode: string;
|
kode: string;
|
||||||
uraian: string;
|
uraian: string;
|
||||||
anggaran: number;
|
anggaran: number;
|
||||||
realisasi: number;
|
|
||||||
selisih: number;
|
|
||||||
persentase: number;
|
|
||||||
level: number;
|
level: number;
|
||||||
tipe?: string | null;
|
tipe?: string | null;
|
||||||
};
|
};
|
||||||
@@ -27,8 +24,7 @@ type FormCreate = {
|
|||||||
|
|
||||||
export default async function apbdesCreate(context: Context) {
|
export default async function apbdesCreate(context: Context) {
|
||||||
const body = context.body as FormCreate;
|
const body = context.body as FormCreate;
|
||||||
|
|
||||||
// Log the incoming request for debugging
|
|
||||||
console.log('Incoming request body:', JSON.stringify(body, null, 2));
|
console.log('Incoming request body:', JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -46,7 +42,7 @@ export default async function apbdesCreate(context: Context) {
|
|||||||
throw new Error('At least one item is required');
|
throw new Error('At least one item is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Buat APBDes + items (tanpa parentId dulu)
|
// 1. Buat APBDes + items dengan auto-calculate fields
|
||||||
const created = await prisma.$transaction(async (prisma) => {
|
const created = await prisma.$transaction(async (prisma) => {
|
||||||
const apbdes = await prisma.aPBDes.create({
|
const apbdes = await prisma.aPBDes.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -59,22 +55,26 @@ export default async function apbdesCreate(context: Context) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create items in a batch
|
// Create items dengan auto-calculate totalRealisasi=0, selisih, persentase
|
||||||
const items = await Promise.all(
|
const items = await Promise.all(
|
||||||
body.items.map(item => {
|
body.items.map(item => {
|
||||||
// Create a new object with only the fields that exist in the APBDesItem model
|
const anggaran = item.anggaran;
|
||||||
|
const totalRealisasi = 0; // Belum ada realisasi saat create
|
||||||
|
const selisih = totalRealisasi - anggaran;
|
||||||
|
const persentase = anggaran > 0 ? (totalRealisasi / anggaran) * 100 : 0;
|
||||||
|
|
||||||
const itemData = {
|
const itemData = {
|
||||||
kode: item.kode,
|
kode: item.kode,
|
||||||
uraian: item.uraian,
|
uraian: item.uraian,
|
||||||
anggaran: item.anggaran,
|
anggaran: anggaran,
|
||||||
realisasi: item.realisasi,
|
|
||||||
selisih: item.selisih,
|
|
||||||
persentase: item.persentase,
|
|
||||||
level: item.level,
|
level: item.level,
|
||||||
tipe: item.tipe, // ✅ sertakan, biar null
|
tipe: item.tipe || null,
|
||||||
|
totalRealisasi,
|
||||||
|
selisih,
|
||||||
|
persentase,
|
||||||
apbdesId: apbdes.id,
|
apbdesId: apbdes.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
return prisma.aPBDesItem.create({
|
return prisma.aPBDesItem.create({
|
||||||
data: itemData,
|
data: itemData,
|
||||||
select: { id: true, kode: true },
|
select: { id: true, kode: true },
|
||||||
@@ -89,20 +89,27 @@ export default async function apbdesCreate(context: Context) {
|
|||||||
// 2. Isi parentId berdasarkan kode
|
// 2. Isi parentId berdasarkan kode
|
||||||
await assignParentIdsToApbdesItems(created.items);
|
await assignParentIdsToApbdesItems(created.items);
|
||||||
|
|
||||||
// 3. Ambil ulang data lengkap untuk response
|
// 3. Ambil ulang data lengkap untuk response (include realisasiItems)
|
||||||
const result = await prisma.aPBDes.findUnique({
|
const result = await prisma.aPBDes.findUnique({
|
||||||
where: { id: created.id },
|
where: { id: created.id },
|
||||||
include: {
|
include: {
|
||||||
image: true,
|
image: true,
|
||||||
file: true,
|
file: true,
|
||||||
items: {
|
items: {
|
||||||
|
where: { isActive: true },
|
||||||
orderBy: { kode: 'asc' },
|
orderBy: { kode: 'asc' },
|
||||||
|
include: {
|
||||||
|
realisasiItems: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: { tanggal: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('APBDes created successfully:', JSON.stringify(result, null, 2));
|
console.log('APBDes created successfully:', JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil membuat APBDes",
|
message: "Berhasil membuat APBDes",
|
||||||
@@ -110,7 +117,6 @@ export default async function apbdesCreate(context: Context) {
|
|||||||
};
|
};
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
console.error('Error in post-creation steps:', innerError);
|
console.error('Error in post-creation steps:', innerError);
|
||||||
// Even if post-creation steps fail, we still return success since the main record was created
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "APBDes berhasil dibuat, tetapi ada masalah dengan pemrosesan tambahan",
|
message: "APBDes berhasil dibuat, tetapi ada masalah dengan pemrosesan tambahan",
|
||||||
@@ -120,13 +126,12 @@ export default async function apbdesCreate(context: Context) {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error creating APBDes:", error);
|
console.error("Error creating APBDes:", error);
|
||||||
|
|
||||||
// Log the full error for debugging
|
|
||||||
if (error.code) console.error('Prisma error code:', error.code);
|
if (error.code) console.error('Prisma error code:', error.code);
|
||||||
if (error.meta) console.error('Prisma error meta:', error.meta);
|
if (error.meta) console.error('Prisma error meta:', error.meta);
|
||||||
|
|
||||||
const errorMessage = error.message || 'Unknown error';
|
const errorMessage = error.message || 'Unknown error';
|
||||||
|
|
||||||
context.set.status = 500;
|
context.set.status = 500;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default async function apbdesFindMany(context: Context) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const where: any = { isActive: true };
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
where.OR = [
|
where.OR = [
|
||||||
{ name: { contains: search, mode: "insensitive" } },
|
{ name: { contains: search, mode: "insensitive" } },
|
||||||
@@ -51,7 +51,10 @@ export default async function apbdesFindMany(context: Context) {
|
|||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
orderBy: { kode: "asc" },
|
orderBy: { kode: "asc" },
|
||||||
include: {
|
include: {
|
||||||
parent: true,
|
realisasiItems: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: { tanggal: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,15 +2,9 @@ import prisma from "@/lib/prisma";
|
|||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function apbdesFindUnique(context: Context) {
|
export default async function apbdesFindUnique(context: Context) {
|
||||||
// ✅ Parse URL secara manual
|
|
||||||
const url = new URL(context.request.url);
|
const url = new URL(context.request.url);
|
||||||
const pathSegments = url.pathname.split('/').filter(Boolean);
|
const pathSegments = url.pathname.split('/').filter(Boolean);
|
||||||
|
|
||||||
console.log("🔍 DEBUG INFO:");
|
|
||||||
console.log("- Full URL:", context.request.url);
|
|
||||||
console.log("- Pathname:", url.pathname);
|
|
||||||
console.log("- Path segments:", pathSegments);
|
|
||||||
|
|
||||||
// Expected: ['api', 'landingpage', 'apbdes', 'ID']
|
// Expected: ['api', 'landingpage', 'apbdes', 'ID']
|
||||||
if (pathSegments.length < 4) {
|
if (pathSegments.length < 4) {
|
||||||
context.set.status = 400;
|
context.set.status = 400;
|
||||||
@@ -20,9 +14,9 @@ export default async function apbdesFindUnique(context: Context) {
|
|||||||
debug: { pathSegments }
|
debug: { pathSegments }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathSegments[0] !== 'api' ||
|
if (pathSegments[0] !== 'api' ||
|
||||||
pathSegments[1] !== 'landingpage' ||
|
pathSegments[1] !== 'landingpage' ||
|
||||||
pathSegments[2] !== 'apbdes') {
|
pathSegments[2] !== 'apbdes') {
|
||||||
context.set.status = 400;
|
context.set.status = 400;
|
||||||
return {
|
return {
|
||||||
@@ -31,9 +25,9 @@ export default async function apbdesFindUnique(context: Context) {
|
|||||||
debug: { pathSegments }
|
debug: { pathSegments }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = pathSegments[3]; // ✅ ID ada di index ke-3
|
const id = pathSegments[3];
|
||||||
|
|
||||||
if (!id || id.trim() === '') {
|
if (!id || id.trim() === '') {
|
||||||
context.set.status = 400;
|
context.set.status = 400;
|
||||||
return {
|
return {
|
||||||
@@ -50,7 +44,10 @@ export default async function apbdesFindUnique(context: Context) {
|
|||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
orderBy: { kode: 'asc' },
|
orderBy: { kode: 'asc' },
|
||||||
include: {
|
include: {
|
||||||
parent: true, // Include parent item for hierarchy
|
realisasiItems: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: { tanggal: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
image: true,
|
image: true,
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ import apbdesDelete from "./del";
|
|||||||
import apbdesFindMany from "./findMany";
|
import apbdesFindMany from "./findMany";
|
||||||
import apbdesFindUnique from "./findUnique";
|
import apbdesFindUnique from "./findUnique";
|
||||||
import apbdesUpdate from "./updt";
|
import apbdesUpdate from "./updt";
|
||||||
|
import realisasiCreate from "./realisasi/create";
|
||||||
|
import realisasiUpdate from "./realisasi/update";
|
||||||
|
import realisasiDelete from "./realisasi/delete";
|
||||||
|
|
||||||
// Definisikan skema untuk item APBDes
|
// Definisikan skema untuk item APBDes (tanpa realisasi field)
|
||||||
const ApbdesItemSchema = t.Object({
|
const ApbdesItemSchema = t.Object({
|
||||||
kode: t.String(),
|
kode: t.String(),
|
||||||
uraian: t.String(),
|
uraian: t.String(),
|
||||||
anggaran: t.Number(),
|
anggaran: t.Number(),
|
||||||
realisasi: t.Number(),
|
|
||||||
selisih: t.Number(),
|
|
||||||
persentase: t.Number(),
|
|
||||||
level: t.Number(),
|
level: t.Number(),
|
||||||
tipe: t.Optional(t.Union([t.String(), t.Null()])) // misal: "pendapatan" atau "belanja"
|
tipe: t.Optional(t.Union([t.String(), t.Null()])), // "pendapatan" | "belanja" | "pembiayaan" | null
|
||||||
});
|
});
|
||||||
|
|
||||||
const APBDes = new Elysia({
|
const APBDes = new Elysia({
|
||||||
@@ -26,10 +26,10 @@ const APBDes = new Elysia({
|
|||||||
// ✅ Find all (dengan query opsional: page, limit, tahun)
|
// ✅ Find all (dengan query opsional: page, limit, tahun)
|
||||||
.get("/findMany", apbdesFindMany)
|
.get("/findMany", apbdesFindMany)
|
||||||
|
|
||||||
// ✅ Find by ID
|
// ✅ Find by ID (include realisasiItems)
|
||||||
.get("/:id", apbdesFindUnique)
|
.get("/:id", apbdesFindUnique)
|
||||||
|
|
||||||
// ✅ Create
|
// ✅ Create APBDes dengan items (tanpa realisasi)
|
||||||
.post("/create", apbdesCreate, {
|
.post("/create", apbdesCreate, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
tahun: t.Number(),
|
tahun: t.Number(),
|
||||||
@@ -42,7 +42,7 @@ const APBDes = new Elysia({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ Update
|
// ✅ Update APBDes dengan items (tanpa realisasi)
|
||||||
.put("/:id", apbdesUpdate, {
|
.put("/:id", apbdesUpdate, {
|
||||||
params: t.Object({ id: t.String() }),
|
params: t.Object({ id: t.String() }),
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
@@ -56,9 +56,40 @@ const APBDes = new Elysia({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ Delete
|
// ✅ Delete APBDes
|
||||||
.delete("/del/:id", apbdesDelete, {
|
.delete("/del/:id", apbdesDelete, {
|
||||||
params: t.Object({ id: t.String() }),
|
params: t.Object({ id: t.String() }),
|
||||||
|
})
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// REALISASI ENDPOINTS
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
// ✅ Create realisasi untuk item tertentu
|
||||||
|
.post("/:itemId/realisasi", realisasiCreate, {
|
||||||
|
params: t.Object({ itemId: t.String() }),
|
||||||
|
body: t.Object({
|
||||||
|
jumlah: t.Number(),
|
||||||
|
tanggal: t.String(),
|
||||||
|
keterangan: t.Optional(t.String()),
|
||||||
|
buktiFileId: t.Optional(t.String()),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Update realisasi
|
||||||
|
.put("/realisasi/:realisasiId", realisasiUpdate, {
|
||||||
|
params: t.Object({ realisasiId: t.String() }),
|
||||||
|
body: t.Object({
|
||||||
|
jumlah: t.Optional(t.Number()),
|
||||||
|
tanggal: t.Optional(t.String()),
|
||||||
|
keterangan: t.Optional(t.String()),
|
||||||
|
buktiFileId: t.Optional(t.String()),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Delete realisasi
|
||||||
|
.delete("/realisasi/:realisasiId", realisasiDelete, {
|
||||||
|
params: t.Object({ realisasiId: t.String() }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default APBDes;
|
export default APBDes;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type RealisasiCreateBody = {
|
||||||
|
jumlah: number;
|
||||||
|
tanggal: string; // ISO format
|
||||||
|
keterangan?: string;
|
||||||
|
buktiFileId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function realisasiCreate(context: Context) {
|
||||||
|
const { itemId } = context.params as { itemId: string };
|
||||||
|
const body = context.body as RealisasiCreateBody;
|
||||||
|
|
||||||
|
console.log('Creating realisasi:', JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Pastikan APBDesItem ada
|
||||||
|
const item = await prisma.aPBDesItem.findUnique({
|
||||||
|
where: { id: itemId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
context.set.status = 404;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Item APBDes tidak ditemukan",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create realisasi item
|
||||||
|
const realisasi = await prisma.realisasiItem.create({
|
||||||
|
data: {
|
||||||
|
apbdesItemId: itemId,
|
||||||
|
jumlah: body.jumlah,
|
||||||
|
tanggal: new Date(body.tanggal),
|
||||||
|
keterangan: body.keterangan,
|
||||||
|
buktiFileId: body.buktiFileId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Update totalRealisasi, selisih, persentase di APBDesItem
|
||||||
|
const allRealisasi = await prisma.realisasiItem.findMany({
|
||||||
|
where: { apbdesItemId: itemId, isActive: true },
|
||||||
|
select: { jumlah: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalRealisasi = allRealisasi.reduce((sum, r) => sum + r.jumlah, 0);
|
||||||
|
const selisih = totalRealisasi - item.anggaran;
|
||||||
|
const persentase = item.anggaran > 0 ? (totalRealisasi / item.anggaran) * 100 : 0;
|
||||||
|
|
||||||
|
await prisma.aPBDesItem.update({
|
||||||
|
where: { id: itemId },
|
||||||
|
data: {
|
||||||
|
totalRealisasi,
|
||||||
|
selisih,
|
||||||
|
persentase,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Return response
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Realisasi berhasil ditambahkan",
|
||||||
|
data: realisasi,
|
||||||
|
meta: {
|
||||||
|
totalRealisasi,
|
||||||
|
selisih,
|
||||||
|
persentase,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error creating realisasi:", error);
|
||||||
|
context.set.status = 500;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Gagal menambahkan realisasi: ${error.message}`,
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function realisasiDelete(context: Context) {
|
||||||
|
const { realisasiId } = context.params as { realisasiId: string };
|
||||||
|
|
||||||
|
console.log('Deleting realisasi:', realisasiId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Pastikan realisasi ada
|
||||||
|
const existing = await prisma.realisasiItem.findUnique({
|
||||||
|
where: { id: realisasiId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
context.set.status = 404;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Realisasi tidak ditemukan",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apbdesItemId = existing.apbdesItemId;
|
||||||
|
|
||||||
|
// 2. Soft delete realisasi (set isActive = false)
|
||||||
|
await prisma.realisasiItem.update({
|
||||||
|
where: { id: realisasiId },
|
||||||
|
data: {
|
||||||
|
isActive: false,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Recalculate totalRealisasi, selisih, persentase di APBDesItem
|
||||||
|
const allRealisasi = await prisma.realisasiItem.findMany({
|
||||||
|
where: { apbdesItemId, isActive: true },
|
||||||
|
select: { jumlah: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await prisma.aPBDesItem.findUnique({
|
||||||
|
where: { id: apbdesItemId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const totalRealisasi = allRealisasi.reduce((sum, r) => sum + r.jumlah, 0);
|
||||||
|
const selisih = totalRealisasi - item.anggaran;
|
||||||
|
const persentase = item.anggaran > 0 ? (totalRealisasi / item.anggaran) * 100 : 0;
|
||||||
|
|
||||||
|
await prisma.aPBDesItem.update({
|
||||||
|
where: { id: apbdesItemId },
|
||||||
|
data: {
|
||||||
|
totalRealisasi,
|
||||||
|
selisih,
|
||||||
|
persentase,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Realisasi berhasil dihapus",
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error deleting realisasi:", error);
|
||||||
|
context.set.status = 500;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Gagal menghapus realisasi: ${error.message}`,
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type RealisasiUpdateBody = {
|
||||||
|
jumlah?: number;
|
||||||
|
tanggal?: string;
|
||||||
|
keterangan?: string;
|
||||||
|
buktiFileId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function realisasiUpdate(context: Context) {
|
||||||
|
const { realisasiId } = context.params as { realisasiId: string };
|
||||||
|
const body = context.body as RealisasiUpdateBody;
|
||||||
|
|
||||||
|
console.log('Updating realisasi:', JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Pastikan realisasi ada
|
||||||
|
const existing = await prisma.realisasiItem.findUnique({
|
||||||
|
where: { id: realisasiId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
context.set.status = 404;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Realisasi tidak ditemukan",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update realisasi
|
||||||
|
const updated = await prisma.realisasiItem.update({
|
||||||
|
where: { id: realisasiId },
|
||||||
|
data: {
|
||||||
|
...(body.jumlah !== undefined && { jumlah: body.jumlah }),
|
||||||
|
...(body.tanggal !== undefined && { tanggal: new Date(body.tanggal) }),
|
||||||
|
...(body.keterangan !== undefined && { keterangan: body.keterangan }),
|
||||||
|
...(body.buktiFileId !== undefined && { buktiFileId: body.buktiFileId }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Recalculate totalRealisasi, selisih, persentase di APBDesItem
|
||||||
|
const allRealisasi = await prisma.realisasiItem.findMany({
|
||||||
|
where: { apbdesItemId: existing.apbdesItemId, isActive: true },
|
||||||
|
select: { jumlah: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await prisma.aPBDesItem.findUnique({
|
||||||
|
where: { id: existing.apbdesItemId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const totalRealisasi = allRealisasi.reduce((sum, r) => sum + r.jumlah, 0);
|
||||||
|
const selisih = totalRealisasi - item.anggaran;
|
||||||
|
const persentase = item.anggaran > 0 ? (totalRealisasi / item.anggaran) * 100 : 0;
|
||||||
|
|
||||||
|
await prisma.aPBDesItem.update({
|
||||||
|
where: { id: existing.apbdesItemId },
|
||||||
|
data: {
|
||||||
|
totalRealisasi,
|
||||||
|
selisih,
|
||||||
|
persentase,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Realisasi berhasil diperbarui",
|
||||||
|
data: updated,
|
||||||
|
meta: {
|
||||||
|
totalRealisasi: allRealisasi.reduce((sum, r) => sum + r.jumlah, 0),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error updating realisasi:", error);
|
||||||
|
context.set.status = 500;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Gagal memperbarui realisasi: ${error.message}`,
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,6 @@ type APBDesItemInput = {
|
|||||||
kode: string;
|
kode: string;
|
||||||
uraian: string;
|
uraian: string;
|
||||||
anggaran: number;
|
anggaran: number;
|
||||||
realisasi: number;
|
|
||||||
selisih: number;
|
|
||||||
persentase: number;
|
|
||||||
level: number;
|
level: number;
|
||||||
tipe?: string | null;
|
tipe?: string | null;
|
||||||
};
|
};
|
||||||
@@ -41,25 +38,32 @@ export default async function apbdesUpdate(context: Context) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Hapus semua item lama
|
// 2. Hapus semua item lama (cascade akan menghapus realisasiItems juga)
|
||||||
await prisma.aPBDesItem.deleteMany({
|
await prisma.aPBDesItem.deleteMany({
|
||||||
where: { apbdesId: id },
|
where: { apbdesId: id },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Buat item baru tanpa parentId terlebih dahulu
|
// 3. Buat item baru dengan auto-calculate fields
|
||||||
await prisma.aPBDesItem.createMany({
|
await prisma.aPBDesItem.createMany({
|
||||||
data: body.items.map((item) => ({
|
data: body.items.map((item) => {
|
||||||
apbdesId: id,
|
const anggaran = item.anggaran;
|
||||||
kode: item.kode,
|
const totalRealisasi = 0; // Reset karena items baru
|
||||||
uraian: item.uraian,
|
const selisih = totalRealisasi - anggaran;
|
||||||
anggaran: item.anggaran,
|
const persentase = anggaran > 0 ? (totalRealisasi / anggaran) * 100 : 0;
|
||||||
realisasi: item.realisasi,
|
|
||||||
selisih: item.anggaran - item.realisasi,
|
return {
|
||||||
persentase: item.anggaran > 0 ? (item.realisasi / item.anggaran) * 100 : 0,
|
apbdesId: id,
|
||||||
level: item.level,
|
kode: item.kode,
|
||||||
tipe: item.tipe || null,
|
uraian: item.uraian,
|
||||||
isActive: true,
|
anggaran: anggaran,
|
||||||
})),
|
level: item.level,
|
||||||
|
tipe: item.tipe || null,
|
||||||
|
totalRealisasi,
|
||||||
|
selisih,
|
||||||
|
persentase,
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Dapatkan semua item yang baru dibuat untuk mendapatkan ID-nya
|
// 4. Dapatkan semua item yang baru dibuat untuk mendapatkan ID-nya
|
||||||
@@ -69,12 +73,11 @@ export default async function apbdesUpdate(context: Context) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 5. Update parentId untuk setiap item
|
// 5. Update parentId untuk setiap item
|
||||||
// Pastikan allItems memiliki tipe yang benar
|
|
||||||
const itemsForParentUpdate = allItems.map(item => ({
|
const itemsForParentUpdate = allItems.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
kode: item.kode,
|
kode: item.kode,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await assignParentIdsToApbdesItems(itemsForParentUpdate);
|
await assignParentIdsToApbdesItems(itemsForParentUpdate);
|
||||||
|
|
||||||
// 6. Update data APBDes
|
// 6. Update data APBDes
|
||||||
@@ -90,13 +93,19 @@ export default async function apbdesUpdate(context: Context) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. Ambil data lengkap untuk response
|
// 7. Ambil data lengkap untuk response (include realisasiItems)
|
||||||
const result = await prisma.aPBDes.findUnique({
|
const result = await prisma.aPBDes.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
items: {
|
items: {
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
orderBy: { kode: 'asc' }
|
orderBy: { kode: 'asc' },
|
||||||
|
include: {
|
||||||
|
realisasiItems: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: { tanggal: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
image: true,
|
image: true,
|
||||||
file: true,
|
file: true,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function Section({ title, data }: any) {
|
|||||||
{item.kode} - {item.uraian}
|
{item.kode} - {item.uraian}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td ta="right">
|
<Table.Td ta="right">
|
||||||
Rp {item.realisasi.toLocaleString('id-ID')}
|
Rp {item.totalRealisasi.toLocaleString('id-ID')}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td ta="center">
|
<Table.Td ta="center">
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
Reference in New Issue
Block a user