Merge pull request 'nico/4-mar-26/realiasasi-apbdes' (#73) from nico/4-mar-26/realiasasi-apbdes into staggingweb

Reviewed-on: #73
This commit is contained in:
2026-03-04 12:03:46 +08:00
55 changed files with 5022 additions and 562 deletions

View File

@@ -1,26 +1,33 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.BeritaGetPayload<{
select: {
judul: true;
deskripsi: true;
content: true;
kategoriBeritaId: true;
imageId: true;
};
}>;
type FormCreate = {
judul: string;
deskripsi: string;
content: string;
kategoriBeritaId: string;
imageId: string; // Featured image
imageIds?: string[]; // Multiple images for gallery
linkVideo?: string; // YouTube link
};
async function beritaCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.berita.create({
data: {
data: {
content: body.content,
deskripsi: body.deskripsi,
imageId: body.imageId,
judul: body.judul,
kategoriBeritaId: body.kategoriBeritaId,
// Connect multiple images if provided
linkVideo: body.linkVideo,
images: body.imageIds && body.imageIds.length > 0
? {
connect: body.imageIds.map((id) => ({ id })),
}
: undefined,
},
});

View File

@@ -28,6 +28,7 @@ export default async function handler(
where: { id },
include: {
image: true,
images: true,
kategoriBerita: true,
},
});

View File

@@ -21,6 +21,8 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
imageId: t.String(),
content: t.String(),
kategoriBeritaId: t.Union([t.String(), t.Null()]),
imageIds: t.Array(t.String()),
linkVideo: t.Optional(t.String()),
}),
})
.get("/find-first", beritaFindFirst)
@@ -39,6 +41,8 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
imageId: t.String(),
content: t.String(),
kategoriBeritaId: t.Union([t.String(), t.Null()]),
imageIds: t.Array(t.String()),
linkVideo: t.Optional(t.String()),
}),
}
);

View File

@@ -4,52 +4,48 @@ import { Prisma } from "@prisma/client";
import fs from "fs/promises";
import path from "path";
type FormUpdate = Prisma.BeritaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
content: true;
kategoriBeritaId: true;
imageId: true;
};
}>;
type FormUpdate = {
id: string;
judul: string;
deskripsi: string;
content: string;
kategoriBeritaId: string;
imageId: string; // Featured image
imageIds?: string[]; // Multiple images for gallery
linkVideo?: string; // YouTube link
};
async function beritaUpdate(context: Context) {
try {
const id = context.params?.id as string; // ambil dari URL
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
judul,
deskripsi,
content,
kategoriBeritaId,
imageId,
} = body;
const { judul, deskripsi, content, kategoriBeritaId, imageId, imageIds, linkVideo } = body;
if (!id) {
return new Response(
JSON.stringify({ success: false, message: "ID tidak boleh kosong" }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
{ status: 400, headers: { "Content-Type": "application/json" } },
);
}
const existing = await prisma.berita.findUnique({
where: { id },
include: {
image: true,
images: true, // Include gallery images
kategoriBerita: true,
},
});
if (!existing) {
return new Response(
JSON.stringify({ success: false, message: "Berita tidak ditemukan" }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
{ status: 404, headers: { "Content-Type": "application/json" } },
);
}
// Delete old featured image if changed
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
@@ -64,35 +60,60 @@ async function beritaUpdate(context: Context) {
}
}
}
// Build update data
const updateData: Prisma.BeritaUpdateInput = {
judul,
deskripsi,
content,
kategoriBerita: kategoriBeritaId ? { connect: { id: kategoriBeritaId } } : { disconnect: true },
image: imageId ? { connect: { id: imageId } } : { disconnect: true },
linkVideo,
};
// Handle multiple images update
if (imageIds !== undefined) {
// Disconnect all existing images first
updateData.images = {
set: [],
};
// Connect new images if provided
if (imageIds.length > 0) {
updateData.images = {
...updateData.images,
connect: imageIds.map((id) => ({ id })),
};
}
}
const updated = await prisma.berita.update({
where: { id },
data: {
judul,
deskripsi,
content,
kategoriBeritaId: kategoriBeritaId || null,
imageId,
data: updateData,
include: {
image: true,
images: true,
kategoriBerita: true,
},
});
return new Response(
JSON.stringify({
success: true,
message: "Berita berhasil diupdate",
data: updated,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
{ status: 200, headers: { "Content-Type": "application/json" } },
);
} catch (error) {
console.error("Error updating berita:", error);
console.error("Error updating berita:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate berita",
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
}
export default beritaUpdate;
export default beritaUpdate;

View File

@@ -2,7 +2,7 @@ import Elysia from "elysia";
import Berita from "./berita";
import Pengumuman from "./pengumuman";
import ProfileDesa from "./profile/profile_desa";
import PotensiDesa from "./potensi";
import PotensiDesa from "./potensi";
import GalleryFoto from "./gallery/foto";
import GalleryVideo from "./gallery/video";
import LayananDesa from "./layanan";
@@ -12,6 +12,7 @@ import KategoriBerita from "./berita/kategori-berita";
import KategoriPengumuman from "./pengumuman/kategori-pengumuman";
import MantanPerbekel from "./profile/profile-mantan-perbekel";
import AjukanPermohonan from "./layanan/ajukan_permohonan";
import Musik from "./musik";
const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
@@ -28,6 +29,7 @@ const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
.use(KategoriBerita)
.use(KategoriPengumuman)
.use(AjukanPermohonan)
.use(Musik)
export default Desa;

View File

@@ -0,0 +1,37 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
type FormCreate = {
judul: string;
artis: string;
deskripsi?: string;
durasi: string;
audioFileId: string;
coverImageId: string;
genre?: string;
tahunRilis?: number | null;
};
async function musikCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.musikDesa.create({
data: {
judul: body.judul,
artis: body.artis,
deskripsi: body.deskripsi,
durasi: body.durasi,
audioFileId: body.audioFileId,
coverImageId: body.coverImageId,
genre: body.genre,
tahunRilis: body.tahunRilis,
},
});
return {
success: true,
message: "Sukses menambahkan musik",
};
}
export default musikCreate;

View File

@@ -0,0 +1,54 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
import path from "path";
const musikDelete = async (context: Context) => {
const { id } = context.params as { id: string };
const musik = await prisma.musikDesa.findUnique({
where: { id },
include: { audioFile: true, coverImage: true },
});
if (!musik) return { status: 404, body: "Musik tidak ditemukan" };
// 1. HAPUS MUSIK DULU
await prisma.musikDesa.delete({ where: { id } });
// 2. HAPUS FILE AUDIO (jika ada)
if (musik.audioFile) {
try {
const fs = await import("fs/promises");
const filePath = path.join(musik.audioFile.path, musik.audioFile.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: musik.audioFile.id },
});
} catch (error) {
console.error("Error deleting audio file:", error);
}
}
// 3. HAPUS FILE COVER (jika ada)
if (musik.coverImage) {
try {
const fs = await import("fs/promises");
const filePath = path.join(musik.coverImage.path, musik.coverImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: musik.coverImage.id },
});
} catch (error) {
console.error("Error deleting cover image:", error);
}
}
return {
success: true,
message: "Musik dan file terkait berhasil dihapus",
};
};
export default musikDelete;

View File

@@ -0,0 +1,66 @@
import prisma from "@/lib/prisma";
export default async function findMusikById(request: Request) {
try {
const url = new URL(request.url);
const id = url.pathname.split("/").pop();
if (!id) {
return new Response(
JSON.stringify({
success: false,
message: "ID tidak valid",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
const data = await prisma.musikDesa.findUnique({
where: { id },
include: {
audioFile: true,
coverImage: true,
},
});
if (!data) {
return new Response(
JSON.stringify({
success: false,
message: "Musik tidak ditemukan",
}),
{
status: 404,
headers: { "Content-Type": "application/json" },
}
);
}
return new Response(
JSON.stringify({
success: true,
message: "Success fetch musik by ID",
data,
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
} catch (e) {
console.error("Error fetching musik by ID:", e);
return new Response(
JSON.stringify({
success: false,
message: "Gagal mengambil musik: " + (e instanceof Error ? e.message : 'Unknown error'),
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
}

View File

@@ -0,0 +1,69 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/desa/musik/find-many.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function musikFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const genre = (context.query.genre as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Filter berdasarkan genre (jika ada)
if (genre) {
where.genre = {
equals: genre,
mode: 'insensitive'
};
}
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ judul: { contains: search, mode: 'insensitive' } },
{ artis: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
{ genre: { contains: search, mode: 'insensitive' } }
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.musikDesa.findMany({
where,
include: {
audioFile: true,
coverImage: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.musikDesa.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data musik dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data musik",
};
}
}
export default musikFindMany;

View File

@@ -0,0 +1,47 @@
import Elysia, { t } from "elysia";
import musikFindMany from "./find-many";
import musikCreate from "./create";
import musikDelete from "./del";
import musikUpdate from "./updt";
import findMusikById from "./find-by-id";
const Musik = new Elysia({ prefix: "/musik", tags: ["Desa/Musik"] })
.get("/find-many", musikFindMany)
.get("/:id", async (context) => {
const response = await findMusikById(new Request(context.request));
return response;
})
.post("/create", musikCreate, {
body: t.Object({
judul: t.String(),
artis: t.String(),
deskripsi: t.Optional(t.String()),
durasi: t.String(),
audioFileId: t.String(),
coverImageId: t.String(),
genre: t.Optional(t.String()),
tahunRilis: t.Optional(t.Number()),
}),
})
.delete("/delete/:id", musikDelete)
.put(
"/:id",
async (context) => {
const response = await musikUpdate(context);
return response;
},
{
body: t.Object({
judul: t.String(),
artis: t.String(),
deskripsi: t.Optional(t.String()),
durasi: t.String(),
audioFileId: t.String(),
coverImageId: t.String(),
genre: t.Optional(t.String()),
tahunRilis: t.Optional(t.Number()),
}),
}
);
export default Musik;

View File

@@ -0,0 +1,65 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
type FormUpdate = {
judul: string;
artis: string;
deskripsi?: string;
durasi: string;
audioFileId: string;
coverImageId: string;
genre?: string;
tahunRilis?: number | null;
};
async function musikUpdate(context: Context) {
const { id } = context.params as { id: string };
const body = context.body as FormUpdate;
try {
const existing = await prisma.musikDesa.findUnique({
where: { id },
});
if (!existing) {
return {
status: 404,
body: {
success: false,
message: "Musik tidak ditemukan",
},
};
}
const updated = await prisma.musikDesa.update({
where: { id },
data: {
judul: body.judul,
artis: body.artis,
deskripsi: body.deskripsi,
durasi: body.durasi,
audioFileId: body.audioFileId,
coverImageId: body.coverImageId,
genre: body.genre,
tahunRilis: body.tahunRilis,
},
});
return {
success: true,
message: "Musik berhasil diupdate",
data: updated,
};
} catch (error) {
console.error("Error updating musik:", error);
return {
status: 500,
body: {
success: false,
message: "Terjadi kesalahan saat mengupdate musik",
},
};
}
}
export default musikUpdate;

View File

@@ -22,9 +22,10 @@ const fileStorageCreate = async (context: Context) => {
if (!UPLOAD_DIR) return { status: 500, body: "UPLOAD_DIR is not defined" };
const isImage = file.type.startsWith("image/");
const category = isImage ? "image" : "document";
const isAudio = file.type.startsWith("audio/");
const category = isImage ? "image" : isAudio ? "audio" : "document";
const pathName = category === "image" ? "images" : "documents";
const pathName = category === "image" ? "images" : category === "audio" ? "audio" : "documents";
const rootPath = path.join(UPLOAD_DIR, pathName);
await fs.mkdir(rootPath, { recursive: true });
@@ -54,6 +55,11 @@ const fileStorageCreate = async (context: Context) => {
// Simpan metadata untuk versi desktop sebagai default
finalName = desktopName;
finalMimeType = "image/webp";
} else if (isAudio) {
// Simpan file audio tanpa kompresi
const ext = file.name.split(".").pop() || "mp3";
finalName = `${finalName}.${ext}`;
await fs.writeFile(path.join(rootPath, finalName), buffer);
} else {
// Jika file adalah PDF, simpan tanpa kompresi
if (file.type === "application/pdf") {

View File

@@ -8,15 +8,15 @@ type APBDesItemInput = {
kode: string;
uraian: string;
anggaran: number;
realisasi: number;
selisih: number;
persentase: number;
level: number;
tipe?: string | null;
};
type FormCreate = {
tahun: number;
name?: string;
deskripsi?: string;
jumlah?: string;
imageId: string;
fileId: string;
items: APBDesItemInput[];
@@ -24,8 +24,7 @@ type FormCreate = {
export default async function apbdesCreate(context: Context) {
const body = context.body as FormCreate;
// Log the incoming request for debugging
console.log('Incoming request body:', JSON.stringify(body, null, 2));
try {
@@ -43,33 +42,39 @@ export default async function apbdesCreate(context: Context) {
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 apbdes = await prisma.aPBDes.create({
data: {
tahun: body.tahun,
name: `APBDes Tahun ${body.tahun}`,
name: body.name || `APBDes Tahun ${body.tahun}`,
deskripsi: body.deskripsi,
jumlah: body.jumlah,
imageId: body.imageId,
fileId: body.fileId,
},
});
// Create items in a batch
// Create items dengan auto-calculate totalRealisasi=0, selisih, persentase
const items = await Promise.all(
body.items.map(item => {
// Create a new object with only the fields that exist in the APBDesItem model
body.items.map(async item => {
const anggaran = item.anggaran;
const totalRealisasi = 0; // Belum ada realisasi saat create
const selisih = anggaran - totalRealisasi; // Sisa anggaran (positif = belum digunakan)
const persentase = anggaran > 0 ? (totalRealisasi / anggaran) * 100 : 0;
const itemData = {
kode: item.kode,
uraian: item.uraian,
anggaran: item.anggaran,
realisasi: item.realisasi,
selisih: item.selisih,
persentase: item.persentase,
anggaran: anggaran,
level: item.level,
tipe: item.tipe, // ✅ sertakan, biar null
tipe: item.tipe || null,
totalRealisasi,
selisih,
persentase,
apbdesId: apbdes.id,
};
return prisma.aPBDesItem.create({
data: itemData,
select: { id: true, kode: true },
@@ -84,20 +89,27 @@ export default async function apbdesCreate(context: Context) {
// 2. Isi parentId berdasarkan kode
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({
where: { id: created.id },
include: {
image: true,
file: true,
items: {
where: { isActive: true },
orderBy: { kode: 'asc' },
include: {
realisasiItems: {
where: { isActive: true },
orderBy: { tanggal: 'asc' },
},
},
},
},
});
console.log('APBDes created successfully:', JSON.stringify(result, null, 2));
return {
success: true,
message: "Berhasil membuat APBDes",
@@ -105,7 +117,6 @@ export default async function apbdesCreate(context: Context) {
};
} catch (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 {
success: true,
message: "APBDes berhasil dibuat, tetapi ada masalah dengan pemrosesan tambahan",
@@ -115,13 +126,12 @@ export default async function apbdesCreate(context: Context) {
}
} catch (error: any) {
console.error("Error creating APBDes:", error);
// Log the full error for debugging
if (error.code) console.error('Prisma error code:', error.code);
if (error.meta) console.error('Prisma error meta:', error.meta);
const errorMessage = error.message || 'Unknown error';
context.set.status = 500;
return {
success: false,

View File

@@ -21,7 +21,7 @@ export default async function apbdesFindMany(context: Context) {
try {
const where: any = { isActive: true };
if (search) {
where.OR = [
{ name: { contains: search, mode: "insensitive" } },
@@ -47,7 +47,16 @@ export default async function apbdesFindMany(context: Context) {
include: {
image: true,
file: true,
items: true,
items: {
where: { isActive: true },
orderBy: { kode: "asc" },
include: {
realisasiItems: {
where: { isActive: true },
orderBy: { tanggal: 'asc' },
},
},
},
},
}),
prisma.aPBDes.count({ where }),

View File

@@ -2,15 +2,9 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function apbdesFindUnique(context: Context) {
// ✅ Parse URL secara manual
const url = new URL(context.request.url);
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']
if (pathSegments.length < 4) {
context.set.status = 400;
@@ -20,9 +14,9 @@ export default async function apbdesFindUnique(context: Context) {
debug: { pathSegments }
};
}
if (pathSegments[0] !== 'api' ||
pathSegments[1] !== 'landingpage' ||
if (pathSegments[0] !== 'api' ||
pathSegments[1] !== 'landingpage' ||
pathSegments[2] !== 'apbdes') {
context.set.status = 400;
return {
@@ -31,9 +25,9 @@ export default async function apbdesFindUnique(context: Context) {
debug: { pathSegments }
};
}
const id = pathSegments[3]; // ✅ ID ada di index ke-3
const id = pathSegments[3];
if (!id || id.trim() === '') {
context.set.status = 400;
return {
@@ -48,11 +42,17 @@ export default async function apbdesFindUnique(context: Context) {
include: {
items: {
where: { isActive: true },
orderBy: { kode: 'asc' }
orderBy: { kode: 'asc' },
include: {
realisasiItems: {
where: { isActive: true },
orderBy: { tanggal: 'asc' },
},
},
},
image: true,
file: true
}
file: true,
},
});
if (!result || !result.isActive) {

View File

@@ -5,17 +5,17 @@ import apbdesDelete from "./del";
import apbdesFindMany from "./findMany";
import apbdesFindUnique from "./findUnique";
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({
kode: t.String(),
uraian: t.String(),
anggaran: t.Number(),
realisasi: t.Number(),
selisih: t.Number(),
persentase: 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({
@@ -26,33 +26,72 @@ const APBDes = new Elysia({
// ✅ Find all (dengan query opsional: page, limit, tahun)
.get("/findMany", apbdesFindMany)
// ✅ Find by ID
// ✅ Find by ID (include realisasiItems)
.get("/:id", apbdesFindUnique)
// ✅ Create
// ✅ Create APBDes dengan items (tanpa realisasi)
.post("/create", apbdesCreate, {
body: t.Object({
tahun: t.Number(),
name: t.Optional(t.String()),
deskripsi: t.Optional(t.String()),
jumlah: t.Optional(t.String()),
imageId: t.String(),
fileId: t.String(),
items: t.Array(ApbdesItemSchema),
}),
})
// ✅ Update
// ✅ Update APBDes dengan items (tanpa realisasi)
.put("/:id", apbdesUpdate, {
params: t.Object({ id: t.String() }),
body: t.Object({
tahun: t.Number(),
name: t.Optional(t.String()),
deskripsi: t.Optional(t.String()),
jumlah: t.Optional(t.String()),
imageId: t.String(),
fileId: t.String(),
items: t.Array(ApbdesItemSchema),
}),
})
// ✅ Delete
// ✅ Delete APBDes
.delete("/del/:id", apbdesDelete, {
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({
kode: t.String(),
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({
kode: t.Optional(t.String()),
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;

View File

@@ -0,0 +1,84 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type RealisasiCreateBody = {
kode: string;
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,
kode: body.kode,
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 = item.anggaran - totalRealisasi; // Sisa anggaran (positif = belum digunakan)
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,
};
}
}

View File

@@ -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 = item.anggaran - totalRealisasi; // Sisa anggaran (positif = belum digunakan)
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,
};
}
}

View File

@@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type RealisasiUpdateBody = {
kode?: string;
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.kode !== undefined && { kode: body.kode }),
...(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 = item.anggaran - totalRealisasi; // Sisa anggaran (positif = belum digunakan)
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,
};
}
}

View File

@@ -6,15 +6,15 @@ type APBDesItemInput = {
kode: string;
uraian: string;
anggaran: number;
realisasi: number;
selisih: number;
persentase: number;
level: number;
tipe?: string | null;
};
type FormUpdateBody = {
tahun: number;
name?: string;
deskripsi?: string;
jumlah?: string;
imageId: string;
fileId: string;
items: APBDesItemInput[];
@@ -38,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({
where: { apbdesId: id },
});
// 3. Buat item baru tanpa parentId terlebih dahulu
// 3. Buat item baru dengan auto-calculate fields
await prisma.aPBDesItem.createMany({
data: body.items.map((item) => ({
apbdesId: id,
kode: item.kode,
uraian: item.uraian,
anggaran: item.anggaran,
realisasi: item.realisasi,
selisih: item.anggaran - item.realisasi,
persentase: item.anggaran > 0 ? (item.realisasi / item.anggaran) * 100 : 0,
level: item.level,
tipe: item.tipe || null,
isActive: true,
})),
data: body.items.map((item) => {
const anggaran = item.anggaran;
const totalRealisasi = 0; // Reset karena items baru
const selisih = anggaran - totalRealisasi; // Sisa anggaran (positif = belum digunakan)
const persentase = anggaran > 0 ? (totalRealisasi / anggaran) * 100 : 0;
return {
apbdesId: id,
kode: item.kode,
uraian: item.uraian,
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
@@ -66,12 +73,11 @@ export default async function apbdesUpdate(context: Context) {
});
// 5. Update parentId untuk setiap item
// Pastikan allItems memiliki tipe yang benar
const itemsForParentUpdate = allItems.map(item => ({
id: item.id,
kode: item.kode,
}));
await assignParentIdsToApbdesItems(itemsForParentUpdate);
// 6. Update data APBDes
@@ -79,18 +85,27 @@ export default async function apbdesUpdate(context: Context) {
where: { id },
data: {
tahun: body.tahun,
name: body.name || `APBDes Tahun ${body.tahun}`,
deskripsi: body.deskripsi,
jumlah: body.jumlah,
imageId: body.imageId,
fileId: body.fileId,
},
});
// 5. Ambil data lengkap untuk response
// 7. Ambil data lengkap untuk response (include realisasiItems)
const result = await prisma.aPBDes.findUnique({
where: { id },
include: {
items: {
where: { isActive: true },
orderBy: { kode: 'asc' }
orderBy: { kode: 'asc' },
include: {
realisasiItems: {
where: { isActive: true },
orderBy: { tanggal: 'asc' },
},
},
},
image: true,
file: true,