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

@@ -1,6 +1,6 @@
{ {
"name": "desa-darmasaba", "name": "desa-darmasaba",
"version": "0.1.54", "version": "0.1.55",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@@ -0,0 +1,30 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const eventBudayaJson = loadJsonData("desa/event-budaya/event-budaya.json");
export async function seedEventBudaya() {
console.log("🔄 Seeding Event Budaya...");
for (const item of eventBudayaJson) {
await prisma.eventBudaya.upsert({
where: { id: item.id },
update: {
nama: item.nama,
tanggal: new Date(item.tanggal),
lokasi: item.lokasi,
deskripsi: item.deskripsi,
},
create: {
id: item.id,
nama: item.nama,
tanggal: new Date(item.tanggal),
lokasi: item.lokasi,
deskripsi: item.deskripsi,
},
});
console.log(` ✅ Event: ${item.nama}`);
}
console.log("🎉 Event Budaya seed selesai");
}

View File

@@ -0,0 +1,58 @@
[
{
"id": "event-budaya-1",
"nama": "Hari Kesaktian Pancasila",
"tanggal": "2025-10-01T07:00:00.000Z",
"lokasi": "Balai Desa Darmasaba",
"deskripsi": "Peringatan Hari Kesaktian Pancasila diikuti seluruh perangkat desa dan warga Desa Darmasaba dengan upacara bendera dan kegiatan budaya."
},
{
"id": "event-budaya-2",
"nama": "Upacara Ngusaba Desa",
"tanggal": "2025-11-15T08:00:00.000Z",
"lokasi": "Pura Puseh Desa Darmasaba",
"deskripsi": "Upacara adat tahunan Ngusaba Desa sebagai bentuk rasa syukur kepada Ida Sang Hyang Widhi Wasa atas keselamatan dan kemakmuran desa."
},
{
"id": "event-budaya-3",
"nama": "Festival Budaya Desa Darmasaba",
"tanggal": "2026-05-20T09:00:00.000Z",
"lokasi": "Lapangan Desa Darmasaba",
"deskripsi": "Festival tahunan menampilkan kesenian tradisional Bali seperti tari kecak, legong, dan barong oleh sanggar seni dari Desa Darmasaba."
},
{
"id": "event-budaya-4",
"nama": "Perayaan HUT Desa Darmasaba",
"tanggal": "2026-08-17T07:30:00.000Z",
"lokasi": "Balai Desa Darmasaba",
"deskripsi": "Peringatan Hari Ulang Tahun Kemerdekaan Republik Indonesia sekaligus hari jadi Desa Darmasaba dengan berbagai lomba dan pertunjukan budaya."
},
{
"id": "event-budaya-5",
"nama": "Perayaan Galungan dan Kuningan",
"tanggal": "2026-03-04T06:00:00.000Z",
"lokasi": "Seluruh wilayah Desa Darmasaba",
"deskripsi": "Rangkaian perayaan Hari Raya Galungan dan Kuningan sebagai hari kemenangan dharma melawan adharma, dirayakan seluruh umat Hindu di Desa Darmasaba."
},
{
"id": "event-budaya-6",
"nama": "Lomba Ogoh-Ogoh Desa",
"tanggal": "2026-03-18T15:00:00.000Z",
"lokasi": "Lapangan Desa Darmasaba",
"deskripsi": "Lomba pembuatan dan parade ogoh-ogoh antar banjar se-Desa Darmasaba dalam rangka menyambut Hari Raya Nyepi."
},
{
"id": "event-budaya-7",
"nama": "Pementasan Wayang Kulit",
"tanggal": "2026-06-10T19:00:00.000Z",
"lokasi": "Wantilan Desa Darmasaba",
"deskripsi": "Pementasan wayang kulit semalam suntuk oleh dalang dari Desa Darmasaba sebagai bagian dari pelestarian seni budaya Bali."
},
{
"id": "event-budaya-8",
"nama": "Upacara Melaspas Gedung Balai Banjar",
"tanggal": "2026-09-05T08:00:00.000Z",
"lokasi": "Banjar Desa Darmasaba",
"deskripsi": "Upacara Melaspas sebagai ritual penyucian bangunan baru balai banjar agar membawa keselamatan dan kesejahteraan bagi krama banjar."
}
]

View File

@@ -0,0 +1,22 @@
-- DropForeignKey
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_kategoriProdukId_fkey";
-- AlterTable
ALTER TABLE "KategoriProdukUmkm" ALTER COLUMN "updatedAt" DROP DEFAULT;
-- CreateTable
CREATE TABLE "EventBudaya" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"tanggal" TIMESTAMP(3) NOT NULL,
"lokasi" TEXT NOT NULL,
"deskripsi" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "EventBudaya_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriProdukId_fkey" FOREIGN KEY ("kategoriProdukId") REFERENCES "KategoriProdukUmkm"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -2553,3 +2553,15 @@ model RingkasanKesehatanDesa {
isActive Boolean @default(true) isActive Boolean @default(true)
} }
// ========================================= EVENT BUDAYA ========================================= //
model EventBudaya {
id String @id @default(cuid())
nama String
tanggal DateTime
lokasi String
deskripsi String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
}

View File

@@ -3,6 +3,7 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { seedBerita } from "./_seeder_list/desa/berita/seed_berita"; import { seedBerita } from "./_seeder_list/desa/berita/seed_berita";
import { seedKegiatanDesa } from "./_seeder_list/desa/seed_kegiatan_desa"; import { seedKegiatanDesa } from "./_seeder_list/desa/seed_kegiatan_desa";
import { seedEventBudaya } from "./_seeder_list/desa/event-budaya/seed_event_budaya";
import { seedFoto } from "./_seeder_list/desa/gallery/foto/seed_foto"; import { seedFoto } from "./_seeder_list/desa/gallery/foto/seed_foto";
import { seedVideo } from "./_seeder_list/desa/gallery/video/seed_video"; import { seedVideo } from "./_seeder_list/desa/gallery/video/seed_video";
import { seedLayanan } from "./_seeder_list/desa/layanan/seed_layanan"; import { seedLayanan } from "./_seeder_list/desa/layanan/seed_layanan";
@@ -392,6 +393,7 @@ import seedAssets from "./seed_assets";
// ===== SOSIAL DASHBOARD ===== // ===== SOSIAL DASHBOARD =====
await seedRingkasanKesehatan(); await seedRingkasanKesehatan();
await seedKegiatanDesa(); await seedKegiatanDesa();
await seedEventBudaya();
// ===== DESA ===== // ===== DESA =====
await seedMusikDesa(); await seedMusikDesa();

View File

@@ -0,0 +1,211 @@
/* 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";
const templateForm = z.object({
nama: z.string().min(1, "Nama event harus diisi"),
tanggal: z.string().min(1, "Tanggal harus diisi"),
lokasi: z.string().min(1, "Lokasi harus diisi"),
deskripsi: z.string().optional(),
});
const defaultForm = {
nama: "",
tanggal: "",
lokasi: "",
deskripsi: "",
};
const eventBudayaState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(eventBudayaState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
eventBudayaState.create.loading = true;
const res = await ApiFetch.api.desa["eventbudaya"]["create"].post(
eventBudayaState.create.form
);
if (res.status === 200 && res.data?.success) {
eventBudayaState.findMany.load();
toast.success("Event budaya berhasil disimpan!");
eventBudayaState.create.form = { ...defaultForm };
return true;
}
toast.error(res.data?.message || "Gagal menyimpan event budaya");
return false;
} catch (error) {
console.error(error);
toast.error("Gagal menyimpan event budaya");
return false;
} finally {
eventBudayaState.create.loading = false;
}
},
},
findMany: {
data: null as Prisma.EventBudayaGetPayload<object>[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
eventBudayaState.findMany.loading = true;
eventBudayaState.findMany.page = page;
eventBudayaState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa["eventbudaya"]["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
eventBudayaState.findMany.data = res.data.data ?? [];
eventBudayaState.findMany.total = res.data.total ?? 0;
eventBudayaState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
eventBudayaState.findMany.data = [];
eventBudayaState.findMany.total = 0;
eventBudayaState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading event budaya:", error);
eventBudayaState.findMany.data = [];
} finally {
eventBudayaState.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.EventBudayaGetPayload<object> | null,
loading: false,
async load(id: string) {
if (!id) return;
this.loading = true;
try {
const res = await fetch(`/api/desa/eventbudaya/${id}`);
if (res.ok) {
const result = await res.json();
eventBudayaState.findUnique.data = result.data ?? null;
}
} catch (error) {
console.error("Error fetching event budaya:", error);
} finally {
this.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) return;
try {
eventBudayaState.edit.loading = true;
const res = await fetch(`/api/desa/eventbudaya/${id}`);
const result = await res.json();
if (result?.success) {
const data = result.data;
eventBudayaState.edit.id = data.id;
eventBudayaState.edit.form = {
nama: data.nama,
tanggal: data.tanggal
? new Date(data.tanggal).toISOString().split("T")[0]
: "",
lokasi: data.lokasi,
deskripsi: data.deskripsi ?? "",
};
}
} catch (error) {
console.error("Error loading event budaya for edit:", error);
} finally {
eventBudayaState.edit.loading = false;
}
},
async save() {
const cek = templateForm.safeParse(eventBudayaState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
eventBudayaState.edit.loading = true;
const res = await fetch(
`/api/desa/eventbudaya/${eventBudayaState.edit.id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(eventBudayaState.edit.form),
}
);
const result = await res.json();
if (result.success) {
toast.success("Event budaya berhasil diupdate");
eventBudayaState.findMany.load();
return true;
}
toast.error(result.message);
return false;
} catch (error) {
console.error(error);
return false;
} finally {
eventBudayaState.edit.loading = false;
}
},
reset() {
eventBudayaState.edit.id = "";
eventBudayaState.edit.form = { ...defaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
eventBudayaState.delete.loading = true;
const res = await fetch(`/api/desa/eventbudaya/del/${id}`, {
method: "DELETE",
});
const result = await res.json();
if (res.ok && result?.success) {
toast.success(result.message || "Event budaya berhasil dihapus");
await eventBudayaState.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus event budaya");
}
} catch (error) {
console.error(error);
toast.error("Gagal menghapus event budaya");
} finally {
eventBudayaState.delete.loading = false;
}
},
},
});
export default eventBudayaState;

View File

@@ -0,0 +1,110 @@
'use client';
import eventBudayaState from '@/app/admin/(dashboard)/_state/desa/eventBudaya';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditEventBudayaPage() {
const state = useProxy(eventBudayaState);
const router = useRouter();
const params = useParams();
const id = params.id as string;
useEffect(() => {
if (id) state.edit.load(id);
return () => state.edit.reset();
}, [id]);
const handleSave = async () => {
const ok = await state.edit.save();
if (ok) router.push('/admin/desa/event-budaya');
};
if (state.edit.loading && !state.edit.form.nama) {
return <Skeleton h={400} radius="md" />;
}
return (
<Box>
<Group mb="lg">
<ActionIcon
variant="light"
size="lg"
onClick={() => router.push('/admin/desa/event-budaya')}
>
<IconArrowBack size={18} />
</ActionIcon>
<Title order={4}>Edit Event Budaya</Title>
</Group>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack gap="md">
<TextInput
label="Nama Event"
placeholder="Contoh: Festival Budaya Desa"
required
value={state.edit.form.nama}
onChange={(e) => (state.edit.form.nama = e.currentTarget.value)}
/>
<TextInput
label="Tanggal"
type="date"
required
value={state.edit.form.tanggal}
onChange={(e) =>
(state.edit.form.tanggal = e.currentTarget.value)
}
/>
<TextInput
label="Lokasi"
placeholder="Contoh: Balai Desa Darmasaba"
required
value={state.edit.form.lokasi}
onChange={(e) => (state.edit.form.lokasi = e.currentTarget.value)}
/>
<Textarea
label="Deskripsi"
placeholder="Deskripsi singkat event (opsional)"
rows={4}
value={state.edit.form.deskripsi}
onChange={(e) =>
(state.edit.form.deskripsi = e.currentTarget.value)
}
/>
<Group justify="flex-end" mt="md">
<Button
variant="default"
onClick={() => router.push('/admin/desa/event-budaya')}
>
Batal
</Button>
<Button
color="blue"
loading={state.edit.loading}
onClick={handleSave}
>
Update
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditEventBudayaPage;

View File

@@ -0,0 +1,95 @@
'use client';
import eventBudayaState from '@/app/admin/(dashboard)/_state/desa/eventBudaya';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Group,
Paper,
Stack,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateEventBudayaPage() {
const state = useProxy(eventBudayaState);
const router = useRouter();
const handleSubmit = async () => {
const ok = await state.create.create();
if (ok) router.push('/admin/desa/event-budaya');
};
return (
<Box>
<Group mb="lg">
<ActionIcon
variant="light"
size="lg"
onClick={() => router.push('/admin/desa/event-budaya')}
>
<IconArrowBack size={18} />
</ActionIcon>
<Title order={4}>Tambah Event Budaya</Title>
</Group>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack gap="md">
<TextInput
label="Nama Event"
placeholder="Contoh: Festival Budaya Desa"
required
value={state.create.form.nama}
onChange={(e) => (state.create.form.nama = e.currentTarget.value)}
/>
<TextInput
label="Tanggal"
type="date"
required
value={state.create.form.tanggal}
onChange={(e) => (state.create.form.tanggal = e.currentTarget.value)}
/>
<TextInput
label="Lokasi"
placeholder="Contoh: Balai Desa Darmasaba"
required
value={state.create.form.lokasi}
onChange={(e) => (state.create.form.lokasi = e.currentTarget.value)}
/>
<Textarea
label="Deskripsi"
placeholder="Deskripsi singkat event (opsional)"
rows={4}
value={state.create.form.deskripsi}
onChange={(e) =>
(state.create.form.deskripsi = e.currentTarget.value)
}
/>
<Group justify="flex-end" mt="md">
<Button
variant="default"
onClick={() => router.push('/admin/desa/event-budaya')}
>
Batal
</Button>
<Button
color="blue"
loading={state.create.loading}
onClick={handleSubmit}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateEventBudayaPage;

View File

@@ -0,0 +1,167 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import eventBudayaState from '@/app/admin/(dashboard)/_state/desa/eventBudaya';
import colors from '@/con/colors';
import {
ActionIcon,
Badge,
Box,
Button,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconCalendarEvent, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
function EventBudayaPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title="Event Budaya"
placeholder="Cari nama atau lokasi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListEventBudaya search={search} />
</Box>
);
}
function ListEventBudaya({ search }: { search: string }) {
const state = useProxy(eventBudayaState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 500);
const { data, page, totalPages, loading, load } = state.findMany;
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
if (loading || !data) {
return (
<Stack py="md">
<Skeleton h={400} radius="md" />
</Stack>
);
}
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="lg">
<Title order={4}>List Event Budaya</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/event-budaya/create')}
>
Tambah Event
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover layout="fixed" withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh w="30%">Nama Event</TableTh>
<TableTh w="20%">Tanggal</TableTh>
<TableTh w="25%">Lokasi</TableTh>
<TableTh w="25%">Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.length > 0 ? (
data.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Group gap="xs">
<IconCalendarEvent size={16} color="blue" />
<Text fz="sm" fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</Group>
</TableTd>
<TableTd>
<Badge variant="light" color="indigo">
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Badge>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" truncate="end" lineClamp={1}>
{item.lokasi}
</Text>
</TableTd>
<TableTd>
<Group gap="xs">
<ActionIcon
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/event-budaya/${item.id}/edit`)
}
>
<IconEdit size={16} />
</ActionIcon>
<ActionIcon
variant="light"
color="red"
loading={state.delete.loading}
onClick={() => state.delete.byId(item.id)}
>
<IconTrash size={16} />
</ActionIcon>
</Group>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Text ta="center" c="dimmed" py="xl">
Belum ada data event budaya
</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{totalPages > 1 && (
<Group justify="center" mt="lg">
<Pagination
total={totalPages}
value={page}
onChange={(p) => load(p, 10, search)}
/>
</Group>
)}
</Paper>
</Box>
);
}
export default EventBudayaPage;

View File

@@ -123,6 +123,11 @@ export const devBar = [
id: "Desa_8", id: "Desa_8",
name: "Kegiatan Desa", name: "Kegiatan Desa",
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa" path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
},
{
id: "Desa_9",
name: "Event Budaya",
path: "/admin/desa/event-budaya"
} }
] ]
@@ -559,6 +564,11 @@ export const navBar = [
id: "Desa_8", id: "Desa_8",
name: "Kegiatan Desa", name: "Kegiatan Desa",
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa" path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
},
{
id: "Desa_9",
name: "Event Budaya",
path: "/admin/desa/event-budaya"
} }
] ]
@@ -1010,6 +1020,11 @@ export const role1 = [
id: "Desa_8", id: "Desa_8",
name: "Kegiatan Desa", name: "Kegiatan Desa",
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa" path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
},
{
id: "Desa_9",
name: "Event Budaya",
path: "/admin/desa/event-budaya"
} }
] ]

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

View File

@@ -2,6 +2,7 @@ import Elysia from "elysia";
import BeasiswaPendaftar from "./beasiswa-pendaftar"; import BeasiswaPendaftar from "./beasiswa-pendaftar";
import KeunggulanProgram from "./keunggulan-program"; import KeunggulanProgram from "./keunggulan-program";
import BeasiswaConfig from "./beasiswa-config"; import BeasiswaConfig from "./beasiswa-config";
import RingkasanBeasiswa from "./ringkasan";
const Beasiswa = new Elysia({ const Beasiswa = new Elysia({
prefix: "/beasiswa", prefix: "/beasiswa",
@@ -10,5 +11,6 @@ const Beasiswa = new Elysia({
.use(BeasiswaPendaftar) .use(BeasiswaPendaftar)
.use(KeunggulanProgram) .use(KeunggulanProgram)
.use(BeasiswaConfig) .use(BeasiswaConfig)
.use(RingkasanBeasiswa)
export default Beasiswa 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 DataPendidikan from "./data-pendidikan";
import Beasiswa from "./beasiswa-desa"; import Beasiswa from "./beasiswa-desa";
import PerpustakaanDigital from "./perpustakaan-digital"; import PerpustakaanDigital from "./perpustakaan-digital";
import RingkasanPendidikan from "./ringkasan";
const Pendidikan = new Elysia({ const Pendidikan = new Elysia({
prefix: "/pendidikan", prefix: "/pendidikan",
@@ -19,5 +20,6 @@ const Pendidikan = new Elysia({
.use(DataPendidikan) .use(DataPendidikan)
.use(Beasiswa) .use(Beasiswa)
.use(PerpustakaanDigital) .use(PerpustakaanDigital)
.use(RingkasanPendidikan)
export default Pendidikan; 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" };
}
}