From 5cbf7810bcd466548a864aa7e0d2f84aefb4eaf6 Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 11 Aug 2025 10:39:06 +0800 Subject: [PATCH] API & UI Admin Menu Desa, Submenu Pengumuman --- .../(dashboard)/_state/desa/pengumuman.ts | 111 ++++++++----- .../list-pengumuman/[id]/edit/page.tsx | 151 ++++++++++++++---- .../pengumuman/list-pengumuman/[id]/page.tsx | 74 +++++---- .../list-pengumuman/create/page.tsx | 53 +++--- .../desa/pengumuman/list-pengumuman/page.tsx | 58 +++---- .../_lib/desa/pengumuman/find-many.ts | 67 ++++++-- .../_lib/desa/pengumuman/index.ts | 3 +- .../[[...slugs]]/_lib/desa/pengumuman/updt.ts | 94 +++++++---- 8 files changed, 398 insertions(+), 213 deletions(-) diff --git a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts index ccb3e16c..8a89aa19 100644 --- a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts +++ b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import ApiFetch from "@/lib/api-fetch"; import { Prisma } from "@prisma/client"; import { toast } from "react-toastify"; @@ -110,7 +111,9 @@ const category = proxy({ } } catch (error) { console.error("Gagal delete:", error); - toast.error("Terjadi kesalahan saat menghapus Data Kategori Pengumuman"); + toast.error( + "Terjadi kesalahan saat menghapus Data Kategori Pengumuman" + ); } finally { category.delete.loading = false; } @@ -150,7 +153,7 @@ const category = proxy({ throw new Error(result?.message || "Gagal memuat data"); } } catch (error) { - console.error("Error loading kategori berita:", error); + console.error("Error loading kategori pengumuman:", error); toast.error( error instanceof Error ? error.message : "Gagal memuat data" ); @@ -170,15 +173,18 @@ const category = proxy({ try { category.update.loading = true; - const response = await fetch(`/api/desa/kategoripengumuman/${this.id}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: this.form.name, - }), - }); + const response = await fetch( + `/api/desa/kategoripengumuman/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + }), + } + ); if (!response.ok) { const errorData = await response.json().catch(() => ({})); @@ -224,17 +230,15 @@ const templateFormPengumuman = z.object({ categoryPengumumanId: z.string().nonempty(), }); -type PengumumanForm = Prisma.PengumumanGetPayload<{ - select: { - judul: true; - deskripsi: true; - content: true; - categoryPengumumanId: true; - }; -}>; +const defaultForm = { + judul: "", + deskripsi: "", + content: "", + categoryPengumumanId: "", +}; const pengumuman = proxy({ create: { - form: {} as PengumumanForm, + form: { ...defaultForm }, loading: false, async create() { const cek = templateFormPengumuman.safeParse(pengumuman.create.form); @@ -270,11 +274,35 @@ const pengumuman = proxy({ }; }>[] | null, - async load() { - const res = await ApiFetch.api.desa.pengumuman["find-many"].get(); - console.log(res); - if (res.status === 200) { - pengumuman.findMany.data = res.data?.data ?? []; + page: 1, + totalPages: 1, + loading: false, + search: "", + load: async (page = 1, limit = 10, search = "", kategori = "") => { + pengumuman.findMany.loading = true; // ✅ Akses langsung via nama path + pengumuman.findMany.page = page; + pengumuman.findMany.search = search; + + try { + const query: any = { page, limit }; + if (search) query.search = search; + if (kategori) query.kategori = kategori; + + const res = await ApiFetch.api.desa.pengumuman["find-many"].get({ query }); + + if (res.status === 200 && res.data?.success) { + pengumuman.findMany.data = res.data.data ?? []; + pengumuman.findMany.totalPages = res.data.totalPages ?? 1; + } else { + pengumuman.findMany.data = []; + pengumuman.findMany.totalPages = 1; + } + } catch (err) { + console.error("Gagal fetch pengumuman paginated:", err); + pengumuman.findMany.data = []; + pengumuman.findMany.totalPages = 1; + } finally { + pengumuman.findMany.loading = false; } }, }, @@ -308,7 +336,7 @@ const pengumuman = proxy({ try { pengumuman.delete.loading = true; - const response = await fetch(`/api/desa/pengumuman/delete/${id}`, { + const response = await fetch(`/api/desa/pengumuman/del/${id}`, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -331,9 +359,9 @@ const pengumuman = proxy({ } }, }, - update: { + edit: { id: "", - form: {} as PengumumanForm, + form: { ...defaultForm }, loading: false, async load(id: string) { @@ -349,6 +377,7 @@ const pengumuman = proxy({ "Content-Type": "application/json", }, }); + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -364,20 +393,21 @@ const pengumuman = proxy({ content: data.content, categoryPengumumanId: data.categoryPengumumanId || "", }; - return data; + return data; // Return the loaded data } else { - throw new Error(result?.message || "Gagal mengambil data pengumuman"); + throw new Error(result?.message || "Gagal memuat data"); } } catch (error) { - console.error((error as Error).message); - toast.error("Terjadi kesalahan saat mengambil data pengumuman"); - } finally { - pengumuman.update.loading = false; + console.error("Error loading pengumuman:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; } }, async update() { - const cek = templateFormPengumuman.safeParse(pengumuman.update.form); + const cek = templateFormPengumuman.safeParse(pengumuman.edit.form); if (!cek.success) { const err = `[${cek.error.issues .map((v) => `${v.path.join(".")}`) @@ -387,7 +417,7 @@ const pengumuman = proxy({ } try { - pengumuman.update.loading = true; + pengumuman.edit.loading = true; const response = await fetch(`/api/desa/pengumuman/${this.id}`, { method: "PUT", @@ -398,7 +428,7 @@ const pengumuman = proxy({ judul: this.form.judul, deskripsi: this.form.deskripsi, content: this.form.content, - categoryPengumumanId: this.form.categoryPengumumanId, + categoryPengumumanId: this.form.categoryPengumumanId || null, }), }); @@ -427,9 +457,14 @@ const pengumuman = proxy({ ); return false; } finally { - pengumuman.update.loading = false; + pengumuman.edit.loading = false; } }, + + reset() { + pengumuman.edit.id = ""; + pengumuman.edit.form = { ...defaultForm }; + }, }, findFirst: { data: null as Prisma.PengumumanGetPayload<{ diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx index ccbb4eb3..68890371 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx @@ -1,52 +1,139 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor'; +/* eslint-disable react-hooks/exhaustive-deps */ +"use client"; + +import EditEditor from "@/app/admin/(dashboard)/_com/editEditor"; +import stateDesaPengumuman from "@/app/admin/(dashboard)/_state/desa/pengumuman"; +import colors from "@/con/colors"; +import { + Box, + Button, + Paper, + Select, + Stack, + Text, + TextInput, + Title +} from "@mantine/core"; +import { IconArrowBack } from "@tabler/icons-react"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { useProxy } from "valtio/utils"; + function EditPengumuman() { + const editState = useProxy(stateDesaPengumuman); const router = useRouter(); + const params = useParams(); + + const [formData, setFormData] = useState({ + judul: editState.pengumuman.edit.form.judul || '', + deskripsi: editState.pengumuman.edit.form.deskripsi || '', + categoryPengumumanId: editState.pengumuman.edit.form.categoryPengumumanId || '', + content: editState.pengumuman.edit.form.content || '' + }); + + // Load pengumuman by id saat pertama kali + useEffect(() => { + editState.category.findMany.load() + const loadpengumuman = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await stateDesaPengumuman.pengumuman.edit.load(id); // akses langsung, bukan dari proxy + if (data) { + setFormData({ + judul: data.judul || '', + deskripsi: data.deskripsi || '', + categoryPengumumanId: data.categoryPengumumanId || '', + content: data.content || '', + }); + } + } catch (error) { + console.error("Error loading pengumuman:", error); + toast.error("Gagal memuat data pengumuman"); + } + }; + + loadpengumuman(); + }, [params?.id]); // ✅ hapus editState dari dependency + + const handleSubmit = async () => { + + try { + // edit global state with form data + editState.pengumuman.edit.form = { + ...editState.pengumuman.edit.form, + judul: formData.judul, + deskripsi: formData.deskripsi, + content: formData.content, + categoryPengumumanId: formData.categoryPengumumanId || '' + }; + await editState.pengumuman.edit.update(); + toast.success("pengumuman berhasil diperbarui!"); + router.push("/admin/desa/pengumuman/list-pengumuman"); + } catch (error) { + console.error("Error updating pengumuman:", error); + toast.error("Terjadi kesalahan saat memperbarui pengumuman"); + } + }; + return ( - - - + - Edit Pengumuman + Edit pengumuman Judul} - placeholder='Masukkan judul' + value={formData.judul} + onChange={(e) => setFormData({ ...formData, judul: e.target.value })} + label={Judul} + placeholder="masukkan judul" /> + Deskripsi Singkat} - placeholder='Masukkan deskripsi singkat' - /> - Tanggal} - placeholder='Masukkan tanggal' - /> - Waktu} - placeholder='Masukkan waktu' + value={formData.deskripsi} + onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })} + label={Deskripsi} + placeholder="masukkan deskripsi" /> - Deskripsi - + Konten + { + setFormData((prev) => ({ ...prev, content: htmlContent })); + editState.pengumuman.edit.form.content = htmlContent; + }} + /> - - - - + Kategori} - placeholder='Pilih kategori' - data={categoryState.findMany.data?.map((item) => ({ - label: item.name, - value: item.id, - }))} - onChange={(val) => { - const selected = categoryState.findMany.data?.find((item) => item.id === val); - if (selected) { - onChange(selected); - } - }} - searchable - nothingFoundMessage="Tidak ditemukan" - /> - ); -} - export default CreatePengumuman; diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx index 97328475..aaf8020b 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx @@ -1,13 +1,12 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { Box, Button, Center, Grid, GridCol, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; -import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import stateDesaPengumuman from '../../../_state/desa/pengumuman'; @@ -30,31 +29,21 @@ function Pengumuman() { function ListPengumuman({ search }: { search: string }) { const pengumumanState = useProxy(stateDesaPengumuman) const router = useRouter() - const [modalHapus, setModalHapus] = useState(false) - const [selectedId, setSelectedId] = useState(null) + const { + data, + page, + totalPages, + loading, + load, + } = pengumumanState.pengumuman.findMany; useShallowEffect(() => { - pengumumanState.pengumuman.findMany.load() - }, []) + load(page, 10, search) + }, [page, search]) + const filteredData = (data || []) - const handleHapus = () => { - if (selectedId) { - pengumumanState.pengumuman.delete.byId(selectedId) - setModalHapus(false) - setSelectedId(null) - } - } - - const filteredData = (pengumumanState.pengumuman.findMany.data || []).filter(item => { - const keyword = search.toLowerCase(); - return ( - item.judul.toLowerCase().includes(keyword) || - item.CategoryPengumuman?.name.toLowerCase().includes(keyword) - ); - }); - - if (!pengumumanState.pengumuman.findMany.data) { + if (loading || !data) { return ( @@ -71,7 +60,7 @@ function ListPengumuman({ search }: { search: string }) { List Pengumuman - @@ -96,7 +85,7 @@ function ListPengumuman({ search }: { search: string }) { {item.CategoryPengumuman?.name} - @@ -107,14 +96,15 @@ function ListPengumuman({ search }: { search: string }) { - - {/* Modal Konfirmasi Hapus */} - setModalHapus(false)} - onConfirm={handleHapus} - text='Apakah anda yakin ingin menghapus pengumuman ini?' - /> +
+ load(newPage)} + total={totalPages} + mt="md" + mb="md" + /> +
) } diff --git a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/find-many.ts b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/find-many.ts index b7574008..82deb793 100644 --- a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/find-many.ts +++ b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/find-many.ts @@ -1,23 +1,70 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// /api/berita/findManyPaginated.ts import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +async function pengumumanFindMany(context: Context) { + // Ambil parameter dari query + const page = Number(context.query.page) || 1; + const limit = Number(context.query.limit) || 10; + const search = (context.query.search as string) || ''; + const kategori = (context.query.kategori as string) || ''; // 🔥 Parameter kategori baru + const skip = (page - 1) * limit; + + // Buat where clause + const where: any = { isActive: true }; + + // Filter berdasarkan kategori (jika ada) + if (kategori) { + where.CategoryPengumuman = { + name: { + equals: kategori, + mode: 'insensitive' // Tidak case-sensitive + } + }; + } + + // Tambahkan pencarian (jika ada) + if (search) { + where.OR = [ + { judul: { contains: search, mode: 'insensitive' } }, + { deskripsi: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + { CategoryPengumuman: { name: { contains: search, mode: 'insensitive' } } } + ]; + } -export default async function pengumumanFindMany() { try { - const data = await prisma.pengumuman.findMany({ - where: { isActive: true }, - include: { - CategoryPengumuman: true, - }, - }); + // Ambil data dan total count secara paralel + const [data, total] = await Promise.all([ + prisma.pengumuman.findMany({ + where, + include: { + CategoryPengumuman: true, + }, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + }), + prisma.pengumuman.count({ where }), + ]); + return { success: true, - message: "Success fetch pengumuman", + message: "Berhasil ambil pengumuman dengan pagination", data, + page, + limit, + total, + totalPages: Math.ceil(total / limit), }; } catch (e) { - console.error("Find many error:", e); + console.error("Error di findMany paginated:", e); return { success: false, - message: "Failed fetch pengumuman", + message: "Gagal mengambil data pengumuman", }; } } + +export default pengumumanFindMany; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/index.ts b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/index.ts index b03ac6e6..b30f4562 100644 --- a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/index.ts +++ b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/index.ts @@ -10,7 +10,7 @@ import pengumumanUpdate from "./updt"; const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"] }) .get("/find-many", pengumumanFindMany) .get("/:id", pengumumanFindById) - .delete("/delete/:id", pengumumanDelete) + .delete("/del/:id", pengumumanDelete) .post("/create", pengumumanCreate, { body: t.Object({ judul: t.String(), @@ -23,7 +23,6 @@ const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"] .get("/find-recent", pengumumanFindRecent) .put("/:id", pengumumanUpdate, { body: t.Object({ - id: t.String(), judul: t.String(), deskripsi: t.String(), content: t.String(), diff --git a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/updt.ts b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/updt.ts index 9a5f2d15..4e4432d1 100644 --- a/src/app/api/[[...slugs]]/_lib/desa/pengumuman/updt.ts +++ b/src/app/api/[[...slugs]]/_lib/desa/pengumuman/updt.ts @@ -3,37 +3,75 @@ import { Prisma } from "@prisma/client"; import { Context } from "elysia"; type FormUpdate = Prisma.PengumumanGetPayload<{ - select: { - id: true; - judul: true; - deskripsi: true; - content: true; - categoryPengumumanId: true; - imageId: true; - }; + select: { + id: true; + judul: true; + deskripsi: true; + content: true; + categoryPengumumanId: true; + }; }>; async function pengumumanUpdate(context: Context) { - const body = context.body as FormUpdate; - - await prisma.pengumuman.update({ - where: { id: body.id }, - data: { - judul: body.judul, - deskripsi: body.deskripsi, - content: body.content, - categoryPengumumanId: body.categoryPengumumanId, - }, + try { + const id = context.params?.id as string; // ambil dari URL + const body = (await context.body) as Omit; + + const { + judul, + deskripsi, + content, + categoryPengumumanId, + } = body; + + if (!id) { + return new Response( + JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const existing = await prisma.pengumuman.findUnique({ + where: { id }, + include: { + CategoryPengumuman: true, + }, }); - - return { + + if (!existing) { + return new Response( + JSON.stringify({ success: false, message: "pengumuman tidak ditemukan" }), + { status: 404, headers: { 'Content-Type': 'application/json' } } + ); + } + + const updated = await prisma.pengumuman.update({ + where: { id }, + data: { + judul, + deskripsi, + content, + categoryPengumumanId: categoryPengumumanId || null, + }, + }); + + return new Response( + JSON.stringify({ success: true, - message: "Success update pengumuman", - data: { - ...body, - }, - }; + message: "pengumuman berhasil diupdate", + data: updated, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error("Error updating pengumuman:", error); + return new Response( + JSON.stringify({ + success: false, + message: "Terjadi kesalahan saat mengupdate pengumuman", + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } } - -export default pengumumanUpdate; - + export default pengumumanUpdate;