upd: list pengaduan

Deskripsi:
- pencarian data list pengaduan

No Issues
This commit is contained in:
2025-11-07 15:19:23 +08:00
parent e0456b2dba
commit 928ecb4c76
4 changed files with 206 additions and 131 deletions

View File

@@ -1,28 +1,28 @@
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY // ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import DarmasabaHome from "./pages/darmasaba/darmasaba_home";
import DarmasabaLayout from "./pages/darmasaba/darmasaba_layout";
import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
import FormKartuTandaPenduduk from "./pages/darmasaba/form_kartu_tanda_penduduk";
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
import FormSuratKeteranganTidakMampu from "./pages/darmasaba/form_surat_keterangan_tidak_mampu";
import FormSuratKeteranganUsaha from "./pages/darmasaba/form_surat_keterangan_usaha";
import DirPage from "./pages/dir/dir_page";
import Home from "./pages/Home";
import Login from "./pages/Login"; import Login from "./pages/Login";
import NotFound from "./pages/NotFound"; import DarmasabaLayout from "./pages/darmasaba/darmasaba_layout";
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page"; import FormSuratKeteranganUsaha from "./pages/darmasaba/form_surat_keterangan_usaha";
import FormSuratKeteranganTidakMampu from "./pages/darmasaba/form_surat_keterangan_tidak_mampu";
import DarmasabaHome from "./pages/darmasaba/darmasaba_home";
import FormKartuTandaPenduduk from "./pages/darmasaba/form_kartu_tanda_penduduk";
import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
import Home from "./pages/Home";
import CredentialPage from "./pages/scr/dashboard/credential/credential_page"; import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
import DashboardHome from "./pages/scr/dashboard/dashboard_home"; import DashboardHome from "./pages/scr/dashboard/dashboard_home";
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
import ListPage from "./pages/scr/dashboard/pengaduan/list_page"; import ListPage from "./pages/scr/dashboard/pengaduan/list_page";
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
import ScrLayout from "./pages/scr/scr_layout"; import ScrLayout from "./pages/scr/scr_layout";
import DirPage from "./pages/dir/dir_page";
import NotFound from "./pages/NotFound";
export default function AppRoutes() { export default function AppRoutes() {
return ( return (

View File

@@ -13,7 +13,7 @@ import { MCPRoute } from "./server/routes/mcp_route";
import PelayananRoute from "./server/routes/pelayanan_surat_route"; import PelayananRoute from "./server/routes/pelayanan_surat_route";
import PengaduanRoute from "./server/routes/pengaduan_route"; import PengaduanRoute from "./server/routes/pengaduan_route";
import UserRoute from "./server/routes/user_route"; import UserRoute from "./server/routes/user_route";
import cors from "@elysiajs/cors" import cors from "@elysiajs/cors";
const Docs = new Elysia({ const Docs = new Elysia({
tags: ["docs"], tags: ["docs"],
@@ -41,11 +41,13 @@ const app = new Elysia()
.use(Api) .use(Api)
.use(Docs) .use(Docs)
.use(Auth) .use(Auth)
.use(cors({ .use(
origin: "*", cors({
methods: ["GET", "POST", "OPTIONS"], origin: "*",
allowedHeaders: ["Content-Type"], methods: ["GET", "POST", "OPTIONS"],
})) allowedHeaders: ["Content-Type"],
}),
)
.get( .get(
"/.well-known/mcp.json", "/.well-known/mcp.json",
async () => { async () => {

View File

@@ -2,17 +2,26 @@ import apiFetch from "@/lib/apiFetch";
import { import {
Badge, Badge,
Card, Card,
CloseButton,
Container, Container,
Divider, Divider,
Flex, Flex,
Group, Group,
Input,
Stack, Stack,
Tabs, Tabs,
Text, Text,
Title Title
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { IconAlignJustified, IconClockHour3, IconFileSad, IconMapPin } from "@tabler/icons-react"; import {
IconAlignJustified,
IconClockHour3,
IconFileSad,
IconMapPin,
IconSearch,
} from "@tabler/icons-react";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr"; import useSwr from "swr";
import { proxy } from "valtio"; import { proxy } from "valtio";
@@ -25,17 +34,13 @@ function reloadState() {
export default function PengaduanListPage() { export default function PengaduanListPage() {
const { search } = useLocation(); const { search } = useLocation();
const query = new URLSearchParams(search); const query = new URLSearchParams(search);
const status = query.get("status"); const status = query.get("status") as StatusKey;
return ( return (
<Container <Container size="xl" py="xl" w={"100%"}>
size="xl"
py="xl"
w={"100%"}
>
<Stack gap="xl"> <Stack gap="xl">
<TabListPengaduan status={status || "all"} /> <TabListPengaduan status={status || "semua"} />
<ListPengaduan status={status || "all"} /> <ListPengaduan status={status || "semua"} />
</Stack> </Stack>
</Container> </Container>
); );
@@ -44,39 +49,83 @@ export default function PengaduanListPage() {
function TabListPengaduan({ status }: { status: string }) { function TabListPengaduan({ status }: { status: string }) {
const navigate = useNavigate(); const navigate = useNavigate();
const dataCount = useSwr("/pengaduan/count", () => const dataCount = useSwr("/pengaduan/count", () =>
apiFetch.api.pengaduan.count.get().then((res) => res.data) apiFetch.api.pengaduan.count.get().then((res) => res.data),
); );
return ( return (
<Tabs defaultValue={status || "all"} color="teal"> <Tabs defaultValue={status || "semua"} color="teal">
<Tabs.List grow> <Tabs.List grow>
<Tabs.Tab value="all" onClick={() => { navigate("?status=all") }}>Semua ({dataCount?.data?.semua || 0})</Tabs.Tab> <Tabs.Tab
<Tabs.Tab value="antrian" onClick={() => { navigate("?status=antrian") }}>Antrian ({dataCount?.data?.antrian || 0})</Tabs.Tab> value="all"
<Tabs.Tab value="diterima" onClick={() => { navigate("?status=diterima") }}>Diterima ({dataCount?.data?.diterima || 0})</Tabs.Tab> onClick={() => {
<Tabs.Tab value="dikerjakan" onClick={() => { navigate("?status=dikerjakan") }}>Dikerjakan ({dataCount?.data?.dikerjakan || 0})</Tabs.Tab> navigate("?status=semua");
<Tabs.Tab value="selesai" onClick={() => { navigate("?status=selesai") }}>Selesai ({dataCount?.data?.selesai || 0})</Tabs.Tab> }}
<Tabs.Tab value="ditolak" onClick={() => { navigate("?status=ditolak") }}>Ditolak ({dataCount?.data?.ditolak || 0})</Tabs.Tab> >
Semua ({dataCount?.data?.semua || 0})
</Tabs.Tab>
<Tabs.Tab
value="antrian"
onClick={() => {
navigate("?status=antrian");
}}
>
Antrian ({dataCount?.data?.antrian || 0})
</Tabs.Tab>
<Tabs.Tab
value="diterima"
onClick={() => {
navigate("?status=diterima");
}}
>
Diterima ({dataCount?.data?.diterima || 0})
</Tabs.Tab>
<Tabs.Tab
value="dikerjakan"
onClick={() => {
navigate("?status=dikerjakan");
}}
>
Dikerjakan ({dataCount?.data?.dikerjakan || 0})
</Tabs.Tab>
<Tabs.Tab
value="selesai"
onClick={() => {
navigate("?status=selesai");
}}
>
Selesai ({dataCount?.data?.selesai || 0})
</Tabs.Tab>
<Tabs.Tab
value="ditolak"
onClick={() => {
navigate("?status=ditolak");
}}
>
Ditolak ({dataCount?.data?.ditolak || 0})
</Tabs.Tab>
</Tabs.List> </Tabs.List>
</Tabs> </Tabs>
); );
} }
function ListPengaduan({ status }: { status: string }) { type StatusKey = "antrian" | "diterima" | "dikerjakan" | "ditolak" | "selesai" | "semua";
function ListPengaduan({ status }: { status: StatusKey }) {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () => const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({ apiFetch.api.pengaduan.list.get({
query: { query: {
status, status,
search: "", search: value,
take: "", take: "",
page: "" page: "",
}, },
}), }),
); );
useShallowEffect(() => { useShallowEffect(() => {
mutate(); mutate();
}, [status]); }, [status, value]);
if (isLoading) if (isLoading)
return ( return (
@@ -99,94 +148,118 @@ function ListPengaduan({ status }: { status: string }) {
return ( return (
<Stack gap="xl"> <Stack gap="xl">
{ <Group grow>
list.length === 0 ? ( <Input
<Flex justify="center" align="center" py={"xl"}> value={value}
<Stack gap={4} align="center"> placeholder="Cari pengaduan..."
<IconFileSad size={32} color="gray" /> onChange={(event) => setValue(event.currentTarget.value)}
<Text c="dimmed" size="sm"> leftSection={<IconSearch size={16} />}
No pengaduan have been added yet. rightSectionPointerEvents="all"
</Text> rightSection={
</Stack> <CloseButton
</Flex> aria-label="Clear input"
) : onClick={() => setValue('')}
list.map((v: any) => ( style={{ display: value ? undefined : 'none' }}
<Card />
key={v.id} }
radius="lg" />
p="xl" {/* <Group justify="flex-end">
withBorder <Text size="sm">Menampilkan {Number(data?.data?.length) * (page - 1) + 1} {Math.min(10, Number(data?.data?.length) * page)} dari {Number(data?.data?.length)}</Text>
style={{ <Pagination total={Number(data?.data?.length)} value={page} onChange={setPage} withPages={false} />
background: </Group> */}
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))", </Group>
borderColor: "rgba(100,100,100,0.2)", {list.length === 0 ? (
boxShadow: "0 0 20px rgba(0,255,200,0.08)", <Flex justify="center" align="center" py={"xl"}>
}} <Stack gap={4} align="center">
> <IconFileSad size={32} color="gray" />
<Stack gap="md"> <Text c="dimmed" size="sm">
<Flex align="center" justify="space-between"> No pengaduan have been added yet.
<Flex direction={"column"}> </Text>
<Title order={3} c="gray.2"> </Stack>
{v.title} </Flex>
) : (
list.map((v: any) => (
<Card
key={v.id}
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={3} c="gray.2">
{v.title}
</Title>
<Group>
<Title order={6} c="gray.5">
#{v.noPengaduan}
</Title> </Title>
<Group> <Text size="sm" c="dimmed">
<Title order={6} c="gray.5"> {v.updatedAt}
#{v.noPengaduan} </Text>
</Title> </Group>
<Text size="sm" c="dimmed">
{v.updatedAt}
</Text>
</Group>
</Flex>
<Badge
size="xl"
variant="light"
radius="sm"
color={v.status === "diterima" ? "green" : v.status === "ditolak" ? "red" : v.status === "selesai" ? "blue" : v.status === "dikerjakan" ? "purple" : "yellow"}
style={{ textTransform: "none" }}
>
{v.status}
</Badge>
</Flex> </Flex>
<Divider my={0} /> <Badge
<Stack gap="sm"> size="xl"
<Flex direction={"column"} justify="flex-start"> variant="light"
<Group gap="xs"> radius="sm"
<IconClockHour3 size={20} color="white" /> color={
<Text size="md" c="white"> v.status === "diterima"
Tanggal Aduan ? "green"
</Text> : v.status === "ditolak"
</Group> ? "red"
<Text size="md"> : v.status === "selesai"
{v.createdAt} ? "blue"
: v.status === "dikerjakan"
? "purple"
: "yellow"
}
style={{ textTransform: "none" }}
>
{v.status}
</Badge>
</Flex>
<Divider my={0} />
<Stack gap="sm">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconClockHour3 size={20} color="white" />
<Text size="md" c="white">
Tanggal Aduan
</Text> </Text>
</Flex> </Group>
<Flex direction={"column"} justify="flex-start"> <Text size="md">{v.createdAt}</Text>
<Group gap="xs"> </Flex>
<IconMapPin size={20} color="white" /> <Flex direction={"column"} justify="flex-start">
<Text size="md" c="white"> <Group gap="xs">
Lokasi <IconMapPin size={20} color="white" />
</Text> <Text size="md" c="white">
</Group> Lokasi
<Text size="md">
{v.location}
</Text> </Text>
</Flex> </Group>
<Flex direction={"column"} justify="flex-start"> <Text size="md">{v.location}</Text>
<Group gap="xs"> </Flex>
<IconAlignJustified size={20} color="white" /> <Flex direction={"column"} justify="flex-start">
<Text size="md" c="white"> <Group gap="xs">
Detail <IconAlignJustified size={20} color="white" />
</Text> <Text size="md" c="white">
</Group> Detail
<Text size="md">
{v.detail}
</Text> </Text>
</Flex> </Group>
</Stack> <Text size="md">{v.detail}</Text>
</Flex>
</Stack> </Stack>
</Card> </Stack>
))} </Card>
))
)}
</Stack> </Stack>
); );
} }

View File

@@ -500,7 +500,7 @@ const PengaduanRoute = new Elysia({
] ]
} }
if (status && status !== "all") { if (status && status !== "semua") {
where = { where = {
...where, ...where,
status: status status: status
@@ -511,7 +511,7 @@ const PengaduanRoute = new Elysia({
skip, skip,
take: !take ? 10 : Number(take), take: !take ? 10 : Number(take),
orderBy: { orderBy: {
createdAt: "asc" createdAt: "desc"
}, },
where, where,
select: { select: {