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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "desa-darmasaba",
|
||||
"version": "0.1.54",
|
||||
"version": "0.1.55",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
30
prisma/_seeder_list/desa/event-budaya/seed_event_budaya.ts
Normal file
30
prisma/_seeder_list/desa/event-budaya/seed_event_budaya.ts
Normal 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");
|
||||
}
|
||||
58
prisma/data/desa/event-budaya/event-budaya.json
Normal file
58
prisma/data/desa/event-budaya/event-budaya.json
Normal 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."
|
||||
}
|
||||
]
|
||||
@@ -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;
|
||||
@@ -2553,3 +2553,15 @@ model RingkasanKesehatanDesa {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { seedBerita } from "./_seeder_list/desa/berita/seed_berita";
|
||||
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 { seedVideo } from "./_seeder_list/desa/gallery/video/seed_video";
|
||||
import { seedLayanan } from "./_seeder_list/desa/layanan/seed_layanan";
|
||||
@@ -392,6 +393,7 @@ import seedAssets from "./seed_assets";
|
||||
// ===== SOSIAL DASHBOARD =====
|
||||
await seedRingkasanKesehatan();
|
||||
await seedKegiatanDesa();
|
||||
await seedEventBudaya();
|
||||
|
||||
// ===== DESA =====
|
||||
await seedMusikDesa();
|
||||
|
||||
211
src/app/admin/(dashboard)/_state/desa/eventBudaya.ts
Normal file
211
src/app/admin/(dashboard)/_state/desa/eventBudaya.ts
Normal 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;
|
||||
110
src/app/admin/(dashboard)/desa/event-budaya/[id]/edit/page.tsx
Normal file
110
src/app/admin/(dashboard)/desa/event-budaya/[id]/edit/page.tsx
Normal 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;
|
||||
95
src/app/admin/(dashboard)/desa/event-budaya/create/page.tsx
Normal file
95
src/app/admin/(dashboard)/desa/event-budaya/create/page.tsx
Normal 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;
|
||||
167
src/app/admin/(dashboard)/desa/event-budaya/page.tsx
Normal file
167
src/app/admin/(dashboard)/desa/event-budaya/page.tsx
Normal 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;
|
||||
@@ -123,6 +123,11 @@ export const devBar = [
|
||||
id: "Desa_8",
|
||||
name: "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",
|
||||
name: "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",
|
||||
name: "Kegiatan Desa",
|
||||
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
|
||||
},
|
||||
{
|
||||
id: "Desa_9",
|
||||
name: "Event Budaya",
|
||||
path: "/admin/desa/event-budaya"
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
29
src/app/api/[[...slugs]]/_lib/desa/event-budaya/create.ts
Normal file
29
src/app/api/[[...slugs]]/_lib/desa/event-budaya/create.ts
Normal 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;
|
||||
20
src/app/api/[[...slugs]]/_lib/desa/event-budaya/del.ts
Normal file
20
src/app/api/[[...slugs]]/_lib/desa/event-budaya/del.ts
Normal 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;
|
||||
46
src/app/api/[[...slugs]]/_lib/desa/event-budaya/find-many.ts
Normal file
46
src/app/api/[[...slugs]]/_lib/desa/event-budaya/find-many.ts
Normal 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;
|
||||
@@ -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;
|
||||
29
src/app/api/[[...slugs]]/_lib/desa/event-budaya/index.ts
Normal file
29
src/app/api/[[...slugs]]/_lib/desa/event-budaya/index.ts
Normal 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;
|
||||
31
src/app/api/[[...slugs]]/_lib/desa/event-budaya/updt.ts
Normal file
31
src/app/api/[[...slugs]]/_lib/desa/event-budaya/updt.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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" };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
46
src/app/api/[[...slugs]]/_lib/pendidikan/ringkasan/stats.ts
Normal file
46
src/app/api/[[...slugs]]/_lib/pendidikan/ringkasan/stats.ts
Normal 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" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user