Fix SDGs Desa Barchart sudah responsive, tabel dan bar progress di menu apbdes sudah sesuai dengan data

This commit is contained in:
2025-11-18 11:56:16 +08:00
parent 9622eb5a9a
commit 0feeb4de93
25 changed files with 2292 additions and 1269 deletions

View File

@@ -1,36 +1,137 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import { assignParentIdsToApbdesItems } from "./lib/getParentsID";
type APBDesItemInput = {
kode: string;
uraian: string;
anggaran: number;
realisasi: number;
selisih: number;
persentase: number;
level: number;
tipe: string;
};
type FormCreate = {
name: string;
jumlah: string;
tahun: number;
imageId: string;
fileId: string;
items: APBDesItemInput[];
};
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 {
const result = await prisma.aPBDes.create({
data: {
name: body.name,
jumlah: body.jumlah,
imageId: body.imageId,
fileId: body.fileId,
},
include: {
image: true,
file: true,
},
// Validate required fields
if (!body.tahun) {
throw new Error('Tahun is required');
}
if (!body.imageId) {
throw new Error('Image ID is required');
}
if (!body.fileId) {
throw new Error('File ID is required');
}
if (!body.items || body.items.length === 0) {
throw new Error('At least one item is required');
}
// 1. Buat APBDes + items (tanpa parentId dulu)
const created = await prisma.$transaction(async (prisma) => {
const apbdes = await prisma.aPBDes.create({
data: {
tahun: body.tahun,
name: `APBDes Tahun ${body.tahun}`,
imageId: body.imageId,
fileId: body.fileId,
},
});
// Create items in a batch
const items = await Promise.all(
body.items.map(item => {
// Create a new object with only the fields that exist in the APBDesItem model
const itemData = {
kode: item.kode,
uraian: item.uraian,
anggaran: item.anggaran,
realisasi: item.realisasi,
selisih: item.selisih,
persentase: item.persentase,
level: item.level,
apbdesId: apbdes.id,
// Note: tipe field is not included as it doesn't exist in the model
};
return prisma.aPBDesItem.create({
data: itemData,
select: { id: true, kode: true },
});
})
);
return { ...apbdes, items };
});
try {
// 2. Isi parentId berdasarkan kode
await assignParentIdsToApbdesItems(created.items);
// 3. Ambil ulang data lengkap untuk response
const result = await prisma.aPBDes.findUnique({
where: { id: created.id },
include: {
image: true,
file: true,
items: {
orderBy: { kode: 'asc' },
},
},
});
console.log('APBDes created successfully:', JSON.stringify(result, null, 2));
return {
success: true,
message: "Berhasil membuat APBDes",
data: result,
};
} 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",
data: created,
warning: process.env.NODE_ENV === 'development' ? String(innerError) : undefined,
};
}
} 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: true,
message: "Berhasil membuat APB Des",
data: result,
success: false,
message: `Gagal membuat APBDes: ${errorMessage}`,
error: process.env.NODE_ENV === 'development' ? {
message: error.message,
code: error.code,
meta: error.meta,
stack: error.stack
} : undefined
};
} catch (error) {
console.error("Error creating APB Des:", error);
throw new Error("Gagal membuat APB Des: " + (error as Error).message);
}
}
}

View File

@@ -2,20 +2,59 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function apbdesDelete(context: Context) {
const { params } = context;
const id = params?.id as string;
const { id } = context.params as { id: string };
if (!id) {
throw new Error("ID tidak ditemukan dalam parameter");
context.set.status = 400;
return {
success: false,
message: "ID APBDes diperlukan",
};
}
const deleted = await prisma.aPBDes.delete({
where: { id },
});
try {
// Cek apakah ada
const existing = await prisma.aPBDes.findUnique({
where: { id },
});
return {
success: true,
message: "Berhasil menghapus APB Des",
data: deleted,
};
if (!existing) {
context.set.status = 404;
return {
success: false,
message: "APBDes tidak ditemukan",
};
}
// Soft delete: set isActive = false & isi deletedAt
const result = await prisma.aPBDes.update({
where: { id },
data: {
isActive: false,
deletedAt: new Date(),
},
});
// Opsional: juga soft delete semua item terkait
await prisma.aPBDesItem.updateMany({
where: { apbdesId: id },
data: {
isActive: false,
deletedAt: new Date()
},
});
return {
success: true,
message: "APBDes berhasil dihapus",
data: result,
};
} catch (error) {
console.error("Error deleting APBDes:", error);
context.set.status = 500;
return {
success: false,
message: "Gagal menghapus APBDes",
};
}
}

View File

@@ -1,56 +1,57 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
// src/app/api/.../apbdes/findMany.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function apbdesFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
export default async function apbdesFindMany(context: Context) {
const { page = "1", limit = "10", tahun } = context.query as {
page?: string;
limit?: string;
tahun?: string;
};
// Buat where clause
const where: any = { isActive: true };
const pageNumber = Math.max(1, parseInt(page, 10) || 1);
const limitNumber = Math.min(100, Math.max(1, parseInt(limit, 10) || 10));
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
const skip = (pageNumber - 1) * limitNumber;
try {
const where: any = { isActive: true };
if (tahun) {
where.tahun = parseInt(tahun, 10);
}
const [data, total] = await Promise.all([
prisma.aPBDes.findMany({
where,
skip,
take: limitNumber,
orderBy: { createdAt: "desc" },
include: {
image: true,
file: true,
items: true,
},
skip,
take: limit,
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.aPBDes.count({
where,
}),
prisma.aPBDes.count({ where }),
]);
return {
success: true,
message: "Success fetch APB Des with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
meta: {
page: pageNumber,
limit: limitNumber,
total,
totalPages: Math.ceil(total / limitNumber),
},
};
} catch (e) {
console.error("Find many paginated error:", e);
} catch (error) {
console.error("Error fetching APBDes list:", error);
context.set.status = 500;
return {
success: false,
message: "Failed fetch APB Des with pagination",
message: "Gagal mengambil daftar APBDes",
};
}
}
export default apbdesFindMany;
}

View File

@@ -2,28 +2,78 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function apbdesFindUnique(context: Context) {
const { params } = context;
const id = params?.id as string;
if (!id) {
throw new Error("ID tidak ditemukan dalam parameter");
// ✅ 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;
return {
success: false,
message: "Route tidak valid. Format: /api/landingpage/apbdes/:id",
debug: { pathSegments }
};
}
if (pathSegments[0] !== 'api' ||
pathSegments[1] !== 'landingpage' ||
pathSegments[2] !== 'apbdes') {
context.set.status = 400;
return {
success: false,
message: "Route tidak valid",
debug: { pathSegments }
};
}
const id = pathSegments[3]; // ✅ ID ada di index ke-3
if (!id || id.trim() === '') {
context.set.status = 400;
return {
success: false,
message: "ID APBDes diperlukan",
};
}
const data = await prisma.aPBDes.findUnique({
where: { id },
include: {
image: true,
file: true,
},
});
try {
const result = await prisma.aPBDes.findUnique({
where: { id },
include: {
items: {
where: { isActive: true },
orderBy: { kode: 'asc' }
},
image: true,
file: true
}
});
if (!data) {
throw new Error("APB Des tidak ditemukan");
if (!result || !result.isActive) {
context.set.status = 404;
return {
success: false,
message: "Data APBDes tidak ditemukan atau tidak aktif",
};
}
return {
success: true,
data: result,
};
} catch (error) {
console.error("❌ Error in apbdesFindUnique:", error);
context.set.status = 500;
return {
success: false,
message: "Terjadi kesalahan saat mengambil data APBDes",
error: process.env.NODE_ENV === 'development' ? String(error) : undefined
};
}
return {
success: true,
message: "Data APB Des ditemukan",
data,
};
}
}

View File

@@ -1,3 +1,4 @@
// src/app/api/[[...slugs]]/_lib/landing_page/apbdes/index.ts
import Elysia, { t } from "elysia";
import apbdesCreate from "./create";
import apbdesDelete from "./del";
@@ -5,12 +6,24 @@ import apbdesFindMany from "./findMany";
import apbdesFindUnique from "./findUnique";
import apbdesUpdate from "./updt";
// Definisikan skema untuk item APBDes
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.String(), // misal: "pendapatan" atau "belanja"
});
const APBDes = new Elysia({
prefix: "/apbdes",
tags: ["Landing Page/Profile/APB Des"],
})
// ✅ Find all
// ✅ Find all (dengan query opsional: page, limit, tahun)
.get("/findMany", apbdesFindMany)
// ✅ Find by ID
@@ -19,23 +32,27 @@ const APBDes = new Elysia({
// ✅ Create
.post("/create", apbdesCreate, {
body: t.Object({
name: t.String(),
tahun: t.Number(),
imageId: t.String(),
fileId: t.String(),
jumlah: t.String(),
items: t.Array(ApbdesItemSchema),
}),
})
// ✅ Update
.put("/:id", apbdesUpdate, {
params: t.Object({ id: t.String() }),
body: t.Object({
name: t.String(),
imageId: t.Optional(t.String()),
fileId: t.Optional(t.String()),
jumlah: t.Optional(t.String()),
tahun: t.Number(),
imageId: t.String(),
fileId: t.String(),
items: t.Array(ApbdesItemSchema),
}),
})
// ✅ Delete
.delete("/del/:id", apbdesDelete);
export default APBDes;
// ✅ Delete
.delete("/del/:id", apbdesDelete, {
params: t.Object({ id: t.String() }),
});
export default APBDes;

View File

@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
/**
* Helper untuk mengisi parentId pada item APBDes berdasarkan struktur kode.
* Harus dipanggil setelah semua item berhasil dibuat (dengan id yang valid).
*
* @param items - Array item yang sudah punya { id, kode }
*/
export async function assignParentIdsToApbdesItems(
items: { id: string; kode: string }[]
): Promise<void> {
// Buat lookup berdasarkan kode → id
const kodeToId = new Map<string, string>();
for (const item of items) {
kodeToId.set(item.kode, item.id);
}
// Siapkan batch update
const updates: Promise<any>[] = [];
for (const item of items) {
const { id, kode } = item;
// Skip jika kode tidak memiliki parent (level 1, misal "4")
if (!kode.includes(".")) {
// Pastikan parentId null untuk akar
updates.push(
prisma.aPBDesItem.update({
where: { id },
data: { parentId: null },
})
);
continue;
}
// Ambil kode parent: "4.1.2" → "4.1"
const parts = kode.split(".");
parts.pop(); // hapus bagian terakhir
const parentKode = parts.join(".");
const parentID = kodeToId.get(parentKode) || null;
updates.push(
prisma.aPBDesItem.update({
where: { id },
data: { parentId: parentID },
})
);
}
// Jalankan semua update secara paralel
await Promise.all(updates);
}

View File

@@ -1,51 +1,113 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import { assignParentIdsToApbdesItems } from "./lib/getParentsID";
type FormUpdateAPBDes = {
name?: string;
imageId?: string;
fileId?: string;
jumlah?: string;
type APBDesItemInput = {
kode: string;
uraian: string;
anggaran: number;
realisasi: number;
selisih: number;
persentase: number;
level: number;
tipe: string;
};
type FormUpdateBody = {
tahun: number;
imageId: string;
fileId: string;
items: APBDesItemInput[];
};
export default async function apbdesUpdate(context: Context) {
const body = context.body as FormUpdateAPBDes;
const id = context.params.id;
if (!id) {
return {
success: false,
message: "ID APB Des wajib diisi",
};
}
const body = context.body as FormUpdateBody;
const { id } = context.params as { id: string };
try {
const updated = await prisma.aPBDes.update({
// 1. Pastikan APBDes ada
const existing = await prisma.aPBDes.findUnique({
where: { id },
});
if (!existing) {
context.set.status = 404;
return {
success: false,
message: "APBDes tidak ditemukan",
};
}
// 2. Hapus semua item lama
await prisma.aPBDesItem.deleteMany({
where: { apbdesId: id },
});
// 3. Buat item baru tanpa parentId terlebih dahulu
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,
isActive: true,
})),
});
// 4. 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
// Pastikan allItems memiliki tipe yang benar
const itemsForParentUpdate = allItems.map(item => ({
id: item.id,
kode: item.kode,
}));
await assignParentIdsToApbdesItems(itemsForParentUpdate);
// 6. Update data APBDes
await prisma.aPBDes.update({
where: { id },
data: {
name: body.name,
tahun: body.tahun,
imageId: body.imageId,
fileId: body.fileId,
jumlah: body.jumlah,
},
});
// 5. Ambil data lengkap untuk response
const result = await prisma.aPBDes.findUnique({
where: { id },
include: {
items: {
where: { isActive: true },
orderBy: { kode: 'asc' }
},
image: true,
file: true,
},
});
return {
success: true,
message: "APB Des berhasil diperbarui",
data: updated,
message: "Berhasil memperbarui APBDes",
data: result,
};
} catch (error: any) {
console.error("Error update APB Des:", error);
} catch (error) {
console.error("Error updating APBDes:", error);
context.set.status = 500;
return {
success: false,
message: "Gagal memperbarui data APB Des",
error: error.message,
message: "Gagal memperbarui APBDes",
};
}
}
}

View File

@@ -3,18 +3,17 @@ import { Context } from "elysia";
type FormCreate = {
namaLengkap: string;
nik: string;
nis: string;
kelas: string;
jenisKelamin: "LAKI_LAKI" | "PEREMPUAN";
alamatDomisili?: string;
tempatLahir: string;
tanggalLahir: string; // ISO date string
jenisKelamin: "LAKI_LAKI" | "PEREMPUAN";
kewarganegaraan: string;
agama: "ISLAM" | "KRISTEN_PROTESTAN" | "KRISTEN_KATOLIK" | "HINDU" | "BUDDHA" | "KONGHUCU" | "LAINNYA";
alamatKTP: string;
alamatDomisili?: string;
namaOrtu?: string;
nik: string;
pekerjaanOrtu?: string;
penghasilan?: string;
noHp: string;
email: string;
statusPernikahan: "BELUM_MENIKAH" | "MENIKAH" | "JANDA_DUDA";
ukuranBaju?: "S" | "M" | "L" | "XL" | "XXL" | "LAINNYA";
};
export default async function beasiswaPendaftarCreate(context: Context) {
@@ -24,18 +23,17 @@ export default async function beasiswaPendaftarCreate(context: Context) {
const result = await prisma.beasiswaPendaftar.create({
data: {
namaLengkap: body.namaLengkap,
nik: body.nik,
nis: body.nis,
kelas: body.kelas,
jenisKelamin: body.jenisKelamin,
alamatDomisili: body.alamatDomisili,
tempatLahir: body.tempatLahir,
tanggalLahir: new Date(body.tanggalLahir),
jenisKelamin: body.jenisKelamin,
kewarganegaraan: body.kewarganegaraan,
agama: body.agama,
alamatKTP: body.alamatKTP,
alamatDomisili: body.alamatDomisili,
namaOrtu: body.namaOrtu,
nik: body.nik,
pekerjaanOrtu: body.pekerjaanOrtu,
penghasilan: body.penghasilan,
noHp: body.noHp,
email: body.email,
statusPernikahan: body.statusPernikahan,
ukuranBaju: body.ukuranBaju,
},
});

View File

@@ -13,18 +13,17 @@ const BeasiswaPendaftar = new Elysia({
.post("/create", beasiswaPendaftarCreate, {
body: t.Object({
namaLengkap: t.String(),
nik: t.String(),
nis: t.String(),
kelas: t.String(),
jenisKelamin: t.String(),
alamatDomisili: t.String(),
tempatLahir: t.String(),
tanggalLahir: t.String(),
jenisKelamin: t.String(),
kewarganegaraan: t.String(),
agama: t.String(),
alamatKTP: t.String(),
alamatDomisili: t.String(),
namaOrtu: t.String(),
nik: t.String(),
pekerjaanOrtu: t.String(),
penghasilan: t.String(),
noHp: t.String(),
email: t.String(),
statusPernikahan: t.String(),
ukuranBaju: t.String(),
}),
})
.get("/findMany", beasiswaPendaftarFindMany)
@@ -43,18 +42,17 @@ const BeasiswaPendaftar = new Elysia({
{
body: t.Object({
namaLengkap: t.String(),
nik: t.String(),
nis: t.String(),
kelas: t.String(),
jenisKelamin: t.String(),
alamatDomisili: t.String(),
tempatLahir: t.String(),
tanggalLahir: t.String(),
jenisKelamin: t.String(),
kewarganegaraan: t.String(),
agama: t.String(),
alamatKTP: t.String(),
alamatDomisili: t.String(),
namaOrtu: t.String(),
nik: t.String(),
pekerjaanOrtu: t.String(),
penghasilan: t.String(),
noHp: t.String(),
email: t.String(),
statusPernikahan: t.String(),
ukuranBaju: t.String(),
}),
}
)

View File

@@ -3,25 +3,17 @@ import { Context } from "elysia";
type FormUpdate = {
namaLengkap: string;
nik: string;
nis: string;
kelas: string;
jenisKelamin: "LAKI_LAKI" | "PEREMPUAN";
alamatDomisili?: string;
tempatLahir: string;
tanggalLahir: string; // ISO date string
jenisKelamin: "LAKI_LAKI" | "PEREMPUAN";
kewarganegaraan: string;
agama:
| "ISLAM"
| "KRISTEN_PROTESTAN"
| "KRISTEN_KATOLIK"
| "HINDU"
| "BUDDHA"
| "KONGHUCU"
| "LAINNYA";
alamatKTP: string;
alamatDomisili?: string;
namaOrtu?: string;
nik: string;
pekerjaanOrtu?: string;
penghasilan?: string;
noHp: string;
email: string;
statusPernikahan: "BELUM_MENIKAH" | "MENIKAH" | "JANDA_DUDA";
ukuranBaju?: "S" | "M" | "L" | "XL" | "XXL" | "LAINNYA";
};
export default async function beasiswaPendaftarUpdate(context: Context) {
@@ -40,18 +32,17 @@ export default async function beasiswaPendaftarUpdate(context: Context) {
where: { id },
data: {
namaLengkap: body.namaLengkap,
nik: body.nik,
nis: body.nis,
kelas: body.kelas,
jenisKelamin: body.jenisKelamin,
alamatDomisili: body.alamatDomisili,
tempatLahir: body.tempatLahir,
tanggalLahir: new Date(body.tanggalLahir),
jenisKelamin: body.jenisKelamin,
kewarganegaraan: body.kewarganegaraan,
agama: body.agama,
alamatKTP: body.alamatKTP,
alamatDomisili: body.alamatDomisili,
namaOrtu: body.namaOrtu,
nik: body.nik,
pekerjaanOrtu: body.pekerjaanOrtu,
penghasilan: body.penghasilan,
noHp: body.noHp,
email: body.email,
statusPernikahan: body.statusPernikahan,
ukuranBaju: body.ukuranBaju,
},
});