From e0456b2dbac5ecc19863824e4b627fa6ad394c69 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Fri, 7 Nov 2025 12:06:46 +0800 Subject: [PATCH] upd: list pengaduan dashboard --- .../scr/dashboard/pengaduan/list_page.tsx | 222 +++++++++--------- src/server/lib/get-last-updated.ts | 21 ++ src/server/routes/pengaduan_route.ts | 133 ++++++++++- 3 files changed, 267 insertions(+), 109 deletions(-) create mode 100644 src/server/lib/get-last-updated.ts diff --git a/src/pages/scr/dashboard/pengaduan/list_page.tsx b/src/pages/scr/dashboard/pengaduan/list_page.tsx index 7682a34..e5fdd04 100644 --- a/src/pages/scr/dashboard/pengaduan/list_page.tsx +++ b/src/pages/scr/dashboard/pengaduan/list_page.tsx @@ -12,11 +12,10 @@ import { Title } from "@mantine/core"; import { useShallowEffect } from "@mantine/hooks"; -import { showNotification } from "@mantine/notifications"; -import { IconAlignJustified, IconClockHour3, IconMapPin } from "@tabler/icons-react"; +import { IconAlignJustified, IconClockHour3, IconFileSad, IconMapPin } from "@tabler/icons-react"; import { useLocation, useNavigate } from "react-router-dom"; import useSwr from "swr"; -import { proxy, subscribe } from "valtio"; +import { proxy } from "valtio"; const state = proxy({ reload: "" }); function reloadState() { @@ -26,9 +25,7 @@ function reloadState() { export default function PengaduanListPage() { const { search } = useLocation(); const query = new URLSearchParams(search); - const status = query.get("status"); - console.log(status, "status"); return ( - + ); @@ -46,47 +43,40 @@ export default function PengaduanListPage() { function TabListPengaduan({ status }: { status: string }) { const navigate = useNavigate(); + const dataCount = useSwr("/pengaduan/count", () => + apiFetch.api.pengaduan.count.get().then((res) => res.data) + ); + return ( - { navigate("?status=all") }}>Semua - { navigate("?status=antrian") }}>Antrian - { navigate("?status=diterima") }}>Diterima - { navigate("?status=dikerjakan") }}>Dikerjakan - { navigate("?status=ditolak") }}>Ditolak - { navigate("?status=selesai") }}>Selesai + { navigate("?status=all") }}>Semua ({dataCount?.data?.semua || 0}) + { navigate("?status=antrian") }}>Antrian ({dataCount?.data?.antrian || 0}) + { navigate("?status=diterima") }}>Diterima ({dataCount?.data?.diterima || 0}) + { navigate("?status=dikerjakan") }}>Dikerjakan ({dataCount?.data?.dikerjakan || 0}) + { navigate("?status=selesai") }}>Selesai ({dataCount?.data?.selesai || 0}) + { navigate("?status=ditolak") }}>Ditolak ({dataCount?.data?.ditolak || 0}) ); } -function ListPengaduan() { +function ListPengaduan({ status }: { status: string }) { const { data, mutate, isLoading } = useSwr("/", () => - apiFetch.api.credential.list.get(), + apiFetch.api.pengaduan.list.get({ + query: { + status, + search: "", + take: "", + page: "" + }, + }), ); useShallowEffect(() => { - const unsubscribe = subscribe(state, () => mutate()); - return () => unsubscribe(); - }, []); + mutate(); + }, [status]); - async function handleRemove(id: string) { - try { - await apiFetch.api.credential.rm.delete({ id }); - showNotification({ - color: "teal", - title: "Credential Deleted", - message: "The credential was successfully removed.", - }); - reloadState(); - } catch { - showNotification({ - color: "red", - title: "Error", - message: "Failed to delete credential. Please try again.", - }); - } - } if (isLoading) return ( @@ -100,87 +90,103 @@ function ListPengaduan() { }} > - Loading credentials... + Loading pengaduan... ); - const list = data?.data?.list || []; + const list = data?.data || []; return ( - - - - - - Dompet Hilang - - - - #PGD-061125-001 - - - updated 2 minutes ago + + { + list.length === 0 ? ( + + + + + No pengaduan have been added yet. - + - - Antrian - - - - - - - - - Tanggal Aduan - - - - 05 November 2025 - - - - - - - Lokasi - - - - Jalan Darmasaba Raya no 77 - - - - - - - Detail - - - - Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis, obcaecati. Sint natus culpa temporibus neque quasi expedita ratione, facere optio incidunt quibusdam suscipit nam nemo delectus beatae similique velit obcaecati? - - - - - + ) : + list.map((v: any) => ( + + + + + + {v.title} + + + + #{v.noPengaduan} + + + {v.updatedAt} + + + + + {v.status} + + + + + + + + + Tanggal Aduan + + + + {v.createdAt} + + + + + + + Lokasi + + + + {v.location} + + + + + + + Detail + + + + {v.detail} + + + + + + ))} + ); } diff --git a/src/server/lib/get-last-updated.ts b/src/server/lib/get-last-updated.ts new file mode 100644 index 0000000..7936390 --- /dev/null +++ b/src/server/lib/get-last-updated.ts @@ -0,0 +1,21 @@ +export function getLastUpdated(date: string | Date): string { + const now = new Date(); + const updated = new Date(date); + const diffMs = now.getTime() - updated.getTime(); + + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMinutes < 1) return "baru saja"; + if (diffMinutes < 60) return `${diffMinutes} menit lalu`; + if (diffHours < 24) return `${diffHours} jam lalu`; + if (diffDays < 7) return `${diffDays} hari lalu`; + + // kalau sudah lebih dari seminggu, tampilkan tanggal + return updated.toLocaleDateString("id-ID", { + day: "numeric", + month: "long", + year: "numeric", + }); +} diff --git a/src/server/routes/pengaduan_route.ts b/src/server/routes/pengaduan_route.ts index cf62aa9..80efa46 100644 --- a/src/server/routes/pengaduan_route.ts +++ b/src/server/routes/pengaduan_route.ts @@ -1,5 +1,6 @@ import Elysia, { t } from "elysia" import type { StatusPengaduan } from "generated/prisma" +import { getLastUpdated } from "../lib/get-last-updated" import { generateNoPengaduan } from "../lib/no-pengaduan" import { normalizePhoneNumber } from "../lib/normalizePhone" import { prisma } from "../lib/prisma" @@ -462,7 +463,137 @@ const PengaduanRoute = new Elysia({ tags: ["mcp"], consumes: ["multipart/form-data"] }, + }) + .get("/list", async ({ query }) => { + const { take, page, search, status } = query + const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take)) + + let where: any = { + isActive: true, + OR: [ + { + title: { + contains: search ?? "", + mode: "insensitive" + }, + }, + { + noPengaduan: { + contains: search ?? "", + mode: "insensitive" + }, + }, + { + detail: { + contains: search ?? "", + mode: "insensitive" + }, + }, + { + Warga: { + phone: { + contains: search ?? "", + mode: "insensitive" + }, + }, + } + ] } - ); + + if (status && status !== "all") { + where = { + ...where, + status: status + } + } + + const data = await prisma.pengaduan.findMany({ + skip, + take: !take ? 10 : Number(take), + orderBy: { + createdAt: "asc" + }, + where, + select: { + id: true, + noPengaduan: true, + title: true, + detail: true, + location: true, + status: true, + createdAt: true, + updatedAt: true, + CategoryPengaduan: { + select: { + name: true + } + }, + Warga: { + select: { + name: true, + } + } + } + }) + + const dataFix = data.map((item) => { + return { + noPengaduan: item.noPengaduan, + title: item.title, + detail: item.detail, + status: item.status, + location: item.location, + createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }), + updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt), + } + }) + + return dataFix + }, { + query: t.Object({ + take: t.String({ optional: true }), + page: t.String({ optional: true }), + search: t.String({ optional: true }), + status: t.String({ optional: true }), + }), + detail: { + summary: "List Pengaduan Warga", + description: `tool untuk mendapatkan list pengaduan warga`, + } + }) + .get("/count", async ({ query }) => { + const counts = await prisma.pengaduan.groupBy({ + by: ['status'], + where: { + isActive: true, + }, + _count: { + status: true, + }, + }); + + const grouped = Object.fromEntries( + counts.map(c => [c.status, c._count.status]) + ); + + const total = await prisma.pengaduan.count({ + where: { isActive: true }, + }); + + return { + antrian: grouped?.antrian || 0, + diterima: grouped?.diterima || 0, + dikerjakan: grouped?.dikerjakan || 0, + ditolak: grouped?.ditolak || 0, + selesai: grouped?.selesai || 0, + semua: total, + }; + }, { + detail: { + summary: "Jumlah Pengaduan Warga", + description: `tool untuk mendapatkan jumlah pengaduan warga`, + } + }) + ; export default PengaduanRoute