Files
desa-darmasaba/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
nico 92b24440fe fix: Quality Control improvements & bug fixes
- APBDes: Fix edit form original data tracking (imageId, fileId)
- APBDes: Update formula consistency in state
- PPID modules: Various UI improvements and bug fixes
- PPID Profil: Preview and edit page improvements
- PPID Dasar Hukum: Page structure improvements
- PPID Visi Misi: Page structure improvements
- PPID Struktur: Posisi organisasi page improvements
- PPID Daftar Informasi: Edit page improvements
- Auth login: Route improvements
- Update dependencies (package.json, bun.lockb)
- Update seed data
- Update .gitignore

QC Reports added:
- QC-APBDES-MODULE.md
- QC-PROFIL-MODULE.md
- QC-SDGS-DESA.md
- QC-DESA-ANTI-KORUPSI.md
- QC-PRESTASI-DESA-MODULE.md
- QC-PPID-PROFIL-MODULE.md
- QC-STRUKTUR-PPID-MODULE.md
- QC-VISI-MISI-PPID-MODULE.md
- QC-DASAR-HUKUM-PPID-MODULE.md
- QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md
- QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md
- QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md
- QC-IKM-MODULE.md

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 14:38:28 +08:00

322 lines
9.3 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// --- Zod Schema ---
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),
realisasi: z.number().min(0),
selisih: z.number(),
persentase: z.number(),
level: z.number().int().min(1).max(3),
tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
});
const ApbdesFormSchema = z.object({
tahun: z.number().int().min(2000, "Tahun tidak valid"),
imageId: z.string().min(1, "Gambar wajib diunggah"),
fileId: z.string().min(1, "File wajib diunggah"),
items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"),
});
// --- Default Form ---
const defaultApbdesForm = {
tahun: new Date().getFullYear(),
imageId: "",
fileId: "",
items: [] as z.infer<typeof ApbdesItemSchema>[],
};
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
function normalizeItem(item: Partial<z.infer<typeof ApbdesItemSchema>>): z.infer<typeof ApbdesItemSchema> {
const anggaran = item.anggaran ?? 0;
const realisasi = item.realisasi ?? 0;
// ✅ Formula yang benar
const selisih = realisasi - anggaran; // positif = sisa anggaran, negatif = over budget
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran
return {
kode: item.kode || "",
uraian: item.uraian || "",
anggaran,
realisasi,
selisih,
persentase,
level: item.level || 1,
tipe: item.tipe, // biarkan null jika memang null
};
}
// --- State Utama ---
const apbdes = proxy({
create: {
form: { ...defaultApbdesForm },
loading: false,
addItem(item: Partial<z.infer<typeof ApbdesItemSchema>>) {
const normalized = normalizeItem(item);
this.form.items.push(normalized);
},
removeItem(index: number) {
this.form.items.splice(index, 1);
},
updateItem(index: number, updates: Partial<z.infer<typeof ApbdesItemSchema>>) {
const current = this.form.items[index];
if (current) {
const updated = normalizeItem({ ...current, ...updates });
this.form.items[index] = updated;
}
},
reset() {
this.form = { ...defaultApbdesForm };
},
async create() {
const parsed = ApbdesFormSchema.safeParse(this.form);
if (!parsed.success) {
const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
toast.error(`Validasi gagal:\n${errors.join("\n")}`);
return;
}
try {
this.loading = true;
const res = await ApiFetch.api.landingpage.apbdes["create"].post(parsed.data);
if (res.data?.success) {
toast.success("APBDes berhasil dibuat");
apbdes.findMany.load();
this.reset();
} else {
toast.error(res.data?.message || "Gagal membuat APBDes");
}
} catch (error: any) {
console.error("Create APBDes error:", error);
toast.error(error?.message || "Terjadi kesalahan saat membuat APBDes");
} finally {
this.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.APBDesGetPayload<{
include: { image: true; file: true; items: true };
}>[]
| null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
apbdes.findMany.loading = true;
apbdes.findMany.page = page;
apbdes.findMany.search = search;
try {
const query: Record<string, string> = { page: String(page), limit: String(limit) };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.apbdes["findMany"].get({ query });
if (res.data?.success) {
apbdes.findMany.data = res.data.data || [];
apbdes.findMany.total = res.data.meta?.total || 0;
apbdes.findMany.totalPages = res.data.meta?.totalPages || 1;
} else {
apbdes.findMany.data = [];
apbdes.findMany.total = 0;
apbdes.findMany.totalPages = 1;
toast.error(res.data?.message || "Gagal memuat data");
}
} catch (error) {
console.error("FindMany error:", error);
apbdes.findMany.data = [];
apbdes.findMany.total = 0;
apbdes.findMany.totalPages = 1;
toast.error("Gagal memuat daftar APBDes");
} finally {
apbdes.findMany.loading = false;
}
},
},
findUnique: {
data: null as
| Prisma.APBDesGetPayload<{
include: { image: true; file: true; items: true };
}>
| null,
loading: false,
error: null as string | null,
async load(id: string) {
if (!id || id.trim() === '') {
this.data = null;
this.error = "ID tidak valid";
return;
}
this.loading = true;
this.error = null;
try {
// Pastikan URL-nya benar
const url = `/api/landingpage/apbdes/${id}`;
console.log("🌐 Fetching:", url);
// Gunakan fetch biasa atau ApiFetch dengan cara yang benar
const response = await fetch(url);
const res = await response.json();
console.log("📦 Response:", res);
if (res.success && res.data) {
this.data = res.data;
} else {
this.data = null;
this.error = res.message || "Gagal memuat detail APBDes";
toast.error(this.error);
}
} catch (error) {
console.error("❌ FindUnique error:", error);
this.data = null;
this.error = "Gagal memuat detail APBDes";
toast.error(this.error);
} finally {
this.loading = false;
}
}
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
this.loading = true;
const res = await (ApiFetch.api.landingpage.apbdes as any)["del"][id].delete();
if (res.data?.success) {
toast.success("APBDes berhasil dihapus");
apbdes.findMany.load();
} else {
toast.error(res.data?.message || "Gagal menghapus APBDes");
}
} catch (error: any) {
console.error("Delete error:", error);
toast.error(error?.message || "Terjadi kesalahan saat menghapus");
} finally {
this.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultApbdesForm },
loading: false,
async load(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
this.loading = true;
const res = await (ApiFetch.api.landingpage.apbdes as any)[id].get();
if (res.data?.success) {
const data = res.data.data;
this.id = data.id;
this.form = {
tahun: data.tahun || new Date().getFullYear(),
imageId: data.imageId || "",
fileId: data.fileId || "",
items: (data.items || []).map((item: any) => ({
kode: item.kode,
uraian: item.uraian,
anggaran: item.anggaran,
realisasi: item.realisasi,
selisih: item.selisih,
persentase: item.persentase,
level: item.level,
tipe: item.tipe || 'pendapatan',
})),
};
return data;
} else {
throw new Error(res.data?.message || "Gagal memuat data");
}
} catch (error: any) {
console.error("Edit load error:", error);
toast.error(error.message || "Gagal memuat data untuk diedit");
} finally {
this.loading = false;
}
},
async update() {
const parsed = ApbdesFormSchema.safeParse(this.form);
if (!parsed.success) {
const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
toast.error(`Validasi gagal:\n${errors.join("\n")}`);
return false;
}
try {
this.loading = true;
// Include the ID in the request body
const requestData = {
...parsed.data,
id: this.id, // Add the ID to the request body
};
const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData);
if (res.data?.success) {
toast.success("APBDes berhasil diperbarui");
apbdes.findMany.load();
return true;
} else {
throw new Error(res.data?.message || "Gagal memperbarui APBDes");
}
} catch (error: any) {
console.error("Update error:", error);
toast.error(error.message || "Gagal memperbarui APBDes");
return false;
} finally {
this.loading = false;
}
},
addItem(item: Partial<z.infer<typeof ApbdesItemSchema>>) {
const normalized = normalizeItem(item);
this.form.items.push(normalized);
},
removeItem(index: number) {
this.form.items.splice(index, 1);
},
reset() {
this.id = "";
this.form = { ...defaultApbdesForm };
},
},
});
export default apbdes;