From ff25ead2dfa64f20a81128f82e81a4b5f3df28e4 Mon Sep 17 00:00:00 2001 From: nico Date: Tue, 5 May 2026 15:25:34 +0800 Subject: [PATCH] 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 --- package.json | 2 +- .../desa/event-budaya/seed_event_budaya.ts | 30 +++ .../data/desa/event-budaya/event-budaya.json | 58 +++++ .../migration.sql | 22 ++ prisma/schema.prisma | 12 + prisma/seed.ts | 2 + .../(dashboard)/_state/desa/eventBudaya.ts | 211 ++++++++++++++++++ .../desa/event-budaya/[id]/edit/page.tsx | 110 +++++++++ .../desa/event-budaya/create/page.tsx | 95 ++++++++ .../(dashboard)/desa/event-budaya/page.tsx | 167 ++++++++++++++ src/app/admin/_com/list_PageAdmin.tsx | 15 ++ .../_lib/desa/event-budaya/create.ts | 29 +++ .../_lib/desa/event-budaya/del.ts | 20 ++ .../_lib/desa/event-budaya/find-many.ts | 46 ++++ .../_lib/desa/event-budaya/findUnique.ts | 23 ++ .../_lib/desa/event-budaya/index.ts | 29 +++ .../_lib/desa/event-budaya/updt.ts | 31 +++ src/app/api/[[...slugs]]/_lib/desa/index.ts | 2 + .../_lib/pendidikan/beasiswa-desa/index.ts | 2 + .../beasiswa-desa/ringkasan/index.ts | 9 + .../beasiswa-desa/ringkasan/stats.ts | 35 +++ .../api/[[...slugs]]/_lib/pendidikan/index.ts | 2 + .../_lib/pendidikan/ringkasan/index.ts | 9 + .../_lib/pendidikan/ringkasan/stats.ts | 46 ++++ 24 files changed, 1006 insertions(+), 1 deletion(-) create mode 100644 prisma/_seeder_list/desa/event-budaya/seed_event_budaya.ts create mode 100644 prisma/data/desa/event-budaya/event-budaya.json create mode 100644 prisma/migrations/20260505070039_add_event_budaya/migration.sql create mode 100644 src/app/admin/(dashboard)/_state/desa/eventBudaya.ts create mode 100644 src/app/admin/(dashboard)/desa/event-budaya/[id]/edit/page.tsx create mode 100644 src/app/admin/(dashboard)/desa/event-budaya/create/page.tsx create mode 100644 src/app/admin/(dashboard)/desa/event-budaya/page.tsx create mode 100644 src/app/api/[[...slugs]]/_lib/desa/event-budaya/create.ts create mode 100644 src/app/api/[[...slugs]]/_lib/desa/event-budaya/del.ts create mode 100644 src/app/api/[[...slugs]]/_lib/desa/event-budaya/find-many.ts create mode 100644 src/app/api/[[...slugs]]/_lib/desa/event-budaya/findUnique.ts create mode 100644 src/app/api/[[...slugs]]/_lib/desa/event-budaya/index.ts create mode 100644 src/app/api/[[...slugs]]/_lib/desa/event-budaya/updt.ts create mode 100644 src/app/api/[[...slugs]]/_lib/pendidikan/beasiswa-desa/ringkasan/index.ts create mode 100644 src/app/api/[[...slugs]]/_lib/pendidikan/beasiswa-desa/ringkasan/stats.ts create mode 100644 src/app/api/[[...slugs]]/_lib/pendidikan/ringkasan/index.ts create mode 100644 src/app/api/[[...slugs]]/_lib/pendidikan/ringkasan/stats.ts diff --git a/package.json b/package.json index 0ab17f3f..4a368d4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "desa-darmasaba", - "version": "0.1.54", + "version": "0.1.55", "private": true, "scripts": { "dev": "next dev", diff --git a/prisma/_seeder_list/desa/event-budaya/seed_event_budaya.ts b/prisma/_seeder_list/desa/event-budaya/seed_event_budaya.ts new file mode 100644 index 00000000..69ee5941 --- /dev/null +++ b/prisma/_seeder_list/desa/event-budaya/seed_event_budaya.ts @@ -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"); +} diff --git a/prisma/data/desa/event-budaya/event-budaya.json b/prisma/data/desa/event-budaya/event-budaya.json new file mode 100644 index 00000000..e96ef631 --- /dev/null +++ b/prisma/data/desa/event-budaya/event-budaya.json @@ -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." + } +] diff --git a/prisma/migrations/20260505070039_add_event_budaya/migration.sql b/prisma/migrations/20260505070039_add_event_budaya/migration.sql new file mode 100644 index 00000000..c3f2918d --- /dev/null +++ b/prisma/migrations/20260505070039_add_event_budaya/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4e9b1419..f451099b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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) +} + diff --git a/prisma/seed.ts b/prisma/seed.ts index c8803e20..c6b13fec 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -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(); diff --git a/src/app/admin/(dashboard)/_state/desa/eventBudaya.ts b/src/app/admin/(dashboard)/_state/desa/eventBudaya.ts new file mode 100644 index 00000000..2b6ac912 --- /dev/null +++ b/src/app/admin/(dashboard)/_state/desa/eventBudaya.ts @@ -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[] | 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 | 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; diff --git a/src/app/admin/(dashboard)/desa/event-budaya/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/event-budaya/[id]/edit/page.tsx new file mode 100644 index 00000000..df713629 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/event-budaya/[id]/edit/page.tsx @@ -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 ; + } + + return ( + + + router.push('/admin/desa/event-budaya')} + > + + + Edit Event Budaya + + + + + (state.edit.form.nama = e.currentTarget.value)} + /> + + (state.edit.form.tanggal = e.currentTarget.value) + } + /> + (state.edit.form.lokasi = e.currentTarget.value)} + /> +