feat(sosial-dashboard): tambah API ringkasan pendidikan & beasiswa + CRUD event budaya - bump 0.1.55

- API GET /api/pendidikan/ringkasan/stats: siswa per jenjang, jumlah lembaga & pengajar
- API GET /api/pendidikan/beasiswa/ringkasan/stats: jumlah penerima, dana, tahun ajaran
- Schema + migration: model EventBudaya (nama, tanggal, lokasi, deskripsi)
- API CRUD /api/desa/eventbudaya: create, find-many, findUnique, updt, del
- State admin: eventBudaya.ts (valtio proxy, create/findMany/edit/delete)
- Admin CMS: /admin/desa/event-budaya (list, create, edit)
- Navbar: tambah entry Desa_9 Event Budaya di semua role
- Seeder: 8 event budaya Bali untuk Desa Darmasaba

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 15:25:34 +08:00
parent 2497298703
commit ff25ead2df
24 changed files with 1006 additions and 1 deletions

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaCreate(context: Context) {
const body = context.body as {
nama: string;
tanggal: string;
lokasi: string;
deskripsi?: string;
};
try {
const data = await prisma.eventBudaya.create({
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
lokasi: body.lokasi,
deskripsi: body.deskripsi || null,
},
});
return { success: true, message: "Event budaya berhasil dibuat", data };
} catch (e) {
console.error("Error di eventBudayaCreate:", e);
return { success: false, message: "Gagal membuat event budaya" };
}
}
export default eventBudayaCreate;

View File

@@ -0,0 +1,20 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaDelete(context: Context) {
const { id } = context.params as { id: string };
try {
await prisma.eventBudaya.update({
where: { id },
data: { isActive: false },
});
return { success: true, message: "Event budaya berhasil dihapus" };
} catch (e) {
console.error("Error di eventBudayaDelete:", e);
return { success: false, message: "Gagal menghapus event budaya" };
}
}
export default eventBudayaDelete;

View File

@@ -0,0 +1,46 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaFindMany(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;
const where: any = { isActive: true };
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ lokasi: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.eventBudaya.findMany({
where,
skip,
take: limit,
orderBy: { tanggal: "asc" },
}),
prisma.eventBudaya.count({ where }),
]);
return {
success: true,
message: "Berhasil mengambil event budaya",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di eventBudayaFindMany:", e);
return { success: false, message: "Gagal mengambil data event budaya" };
}
}
export default eventBudayaFindMany;

View File

@@ -0,0 +1,23 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaFindUnique(context: Context) {
const { id } = context.params as { id: string };
try {
const data = await prisma.eventBudaya.findFirst({
where: { id, isActive: true },
});
if (!data) {
return { success: false, message: "Event budaya tidak ditemukan" };
}
return { success: true, data };
} catch (e) {
console.error("Error di eventBudayaFindUnique:", e);
return { success: false, message: "Gagal mengambil data event budaya" };
}
}
export default eventBudayaFindUnique;

View File

@@ -0,0 +1,29 @@
import Elysia, { t } from "elysia";
import eventBudayaFindMany from "./find-many";
import eventBudayaFindUnique from "./findUnique";
import eventBudayaCreate from "./create";
import eventBudayaDelete from "./del";
import eventBudayaUpdate from "./updt";
const EventBudaya = new Elysia({ prefix: "/eventbudaya", tags: ["Desa/Event Budaya"] })
.get("/find-many", eventBudayaFindMany)
.get("/:id", eventBudayaFindUnique)
.post("/create", eventBudayaCreate, {
body: t.Object({
nama: t.String(),
tanggal: t.String(),
lokasi: t.String(),
deskripsi: t.Optional(t.String()),
}),
})
.put("/:id", eventBudayaUpdate, {
body: t.Object({
nama: t.String(),
tanggal: t.String(),
lokasi: t.String(),
deskripsi: t.Optional(t.String()),
}),
})
.delete("/del/:id", eventBudayaDelete);
export default EventBudaya;

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaUpdate(context: Context) {
const { id } = context.params as { id: string };
const body = context.body as {
nama: string;
tanggal: string;
lokasi: string;
deskripsi?: string;
};
try {
const data = await prisma.eventBudaya.update({
where: { id },
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
lokasi: body.lokasi,
deskripsi: body.deskripsi || null,
},
});
return { success: true, message: "Event budaya berhasil diupdate", data };
} catch (e) {
console.error("Error di eventBudayaUpdate:", e);
return { success: false, message: "Gagal mengupdate event budaya" };
}
}
export default eventBudayaUpdate;

View File

@@ -15,6 +15,7 @@ import AjukanPermohonan from "./layanan/ajukan_permohonan";
import Musik from "./musik";
import KegiatanDesa from "./kegiatan-desa";
import KategoriKegiatan from "./kegiatan-desa/kategori-kegiatan";
import EventBudaya from "./event-budaya";
const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
@@ -34,6 +35,7 @@ const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
.use(Musik)
.use(KegiatanDesa)
.use(KategoriKegiatan)
.use(EventBudaya)
export default Desa;

View File

@@ -2,6 +2,7 @@ import Elysia from "elysia";
import BeasiswaPendaftar from "./beasiswa-pendaftar";
import KeunggulanProgram from "./keunggulan-program";
import BeasiswaConfig from "./beasiswa-config";
import RingkasanBeasiswa from "./ringkasan";
const Beasiswa = new Elysia({
prefix: "/beasiswa",
@@ -10,5 +11,6 @@ const Beasiswa = new Elysia({
.use(BeasiswaPendaftar)
.use(KeunggulanProgram)
.use(BeasiswaConfig)
.use(RingkasanBeasiswa)
export default Beasiswa

View File

@@ -0,0 +1,9 @@
import Elysia from "elysia";
import beasiswaRingkasanStats from "./stats";
const RingkasanBeasiswa = new Elysia({
prefix: "/ringkasan",
tags: ["Pendidikan/Beasiswa Desa"],
}).get("/stats", beasiswaRingkasanStats);
export default RingkasanBeasiswa;

View File

@@ -0,0 +1,35 @@
import prisma from "@/lib/prisma";
type StatsResult = {
jumlahPenerima: number;
danaTersalurkan: bigint;
tahunAjaran: string;
};
export default async function beasiswaRingkasanStats(): Promise<{
success: boolean;
data?: StatsResult;
message?: string;
}> {
try {
const [jumlahPenerima, config] = await Promise.all([
prisma.beasiswaPendaftar.count({ where: { isActive: true } }),
prisma.beasiswaConfig.findFirst({
where: { isActive: true },
orderBy: { createdAt: "desc" },
}),
]);
return {
success: true,
data: {
jumlahPenerima,
danaTersalurkan: config?.danaTersalurkan ?? BigInt(0),
tahunAjaran: config?.tahunAjaran ?? "-",
},
};
} catch (e) {
console.error("beasiswaRingkasanStats error:", e);
return { success: false, message: "Gagal menghitung statistik beasiswa" };
}
}

View File

@@ -6,6 +6,7 @@ import PendidikanNonFormal from "./pendidikan-non-formal";
import DataPendidikan from "./data-pendidikan";
import Beasiswa from "./beasiswa-desa";
import PerpustakaanDigital from "./perpustakaan-digital";
import RingkasanPendidikan from "./ringkasan";
const Pendidikan = new Elysia({
prefix: "/pendidikan",
@@ -19,5 +20,6 @@ const Pendidikan = new Elysia({
.use(DataPendidikan)
.use(Beasiswa)
.use(PerpustakaanDigital)
.use(RingkasanPendidikan)
export default Pendidikan;

View File

@@ -0,0 +1,9 @@
import Elysia from "elysia";
import pendidikanRingkasanStats from "./stats";
const RingkasanPendidikan = new Elysia({
prefix: "/ringkasan",
tags: ["Pendidikan/Ringkasan"],
}).get("/stats", pendidikanRingkasanStats);
export default RingkasanPendidikan;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
type PerJenjang = { nama: string; jumlahSiswa: number };
type StatsResult = {
perJenjang: PerJenjang[];
jumlahLembaga: number;
jumlahPengajar: number;
};
export default async function pendidikanRingkasanStats(): Promise<{
success: boolean;
data?: StatsResult;
message?: string;
}> {
try {
const [jenjangList, jumlahLembaga, jumlahPengajar] = await Promise.all([
prisma.jenjangPendidikan.findMany({
where: { isActive: true },
include: {
lembagas: {
where: { isActive: true },
include: {
_count: { select: { siswa: { where: { isActive: true } } } },
},
},
},
}),
prisma.lembaga.count({ where: { isActive: true } }),
prisma.pengajar.count({ where: { isActive: true } }),
]);
const perJenjang = jenjangList.map((j) => ({
nama: j.nama,
jumlahSiswa: j.lembagas.reduce((acc, l) => acc + l._count.siswa, 0),
}));
return {
success: true,
data: { perJenjang, jumlahLembaga, jumlahPengajar },
};
} catch (e) {
console.error("pendidikanRingkasanStats error:", e);
return { success: false, message: "Gagal menghitung statistik pendidikan" };
}
}