Merge pull request 'upd : dahboard admin' (#28) from amalia/17-nov-25 into main

Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/28
This commit is contained in:
2025-11-17 13:42:32 +08:00
3 changed files with 122 additions and 98 deletions

View File

@@ -17,7 +17,7 @@ import {
Textarea, Textarea,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { import {
IconAlignJustified, IconAlignJustified,
IconCategory, IconCategory,
@@ -28,53 +28,68 @@ import {
IconPhotoScan, IconPhotoScan,
IconUser, IconUser,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import _ from "lodash";
import { useState } from "react"; import { useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import useSwr from "swr"; import useSwr from "swr";
import { useShallowEffect } from "@mantine/hooks";
export default function DetailPengaduanPage() { export default function DetailPengaduanPage() {
const { search } = useLocation(); const { search } = useLocation();
const query = new URLSearchParams(search); const query = new URLSearchParams(search);
const id = query.get("id"); const id = query.get("id");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.detail.get({
query: {
id: id!,
},
}),
);
useShallowEffect(() => {
mutate();
}, []);
return ( return (
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
<Grid> <Grid>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataPengaduan /> <DetailDataPengaduan data={data?.data?.pengaduan} />
<DetailDataHistori /> <DetailDataHistori data={data?.data?.history} />
</Stack> </Stack>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col span={4}>
<DetailUserPengaduan /> <DetailUserPengaduan data={data?.data?.warga} />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Container> </Container>
); );
} }
function DetailDataPengaduan() { function DetailDataPengaduan({ data }: { data: any }) {
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak"); const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [imageSrc, setImageSrc] = useState<string | null>(null); const [imageSrc, setImageSrc] = useState<string | null>(null);
const [openedModalImage, { open: openModalImage, close: closeModalImage }] = const [openedModalImage, { open: openModalImage, close: closeModalImage }] =
useDisclosure(false); useDisclosure(false);
async function handleLihatGambar() { // async function handleLihatGambar() {
const res = await apiFetch.api.pengaduan.image.get({ // const res = await apiFetch.api.pengaduan.image.get({
query: { // query: {
fileName: "57d5ce89-7d18-4244-9f4c-ca21b70adb7e", // fileName: "57d5ce89-7d18-4244-9f4c-ca21b70adb7e",
}, // },
}); // });
console.error("client", res); // console.error("client", res);
// const blob = await res.data?.blob(); // // const blob = await res.data?.blob();
// setImageSrc(URL.createObjectURL(blob!)); // // setImageSrc(URL.createObjectURL(blob!));
// openModalImage(); // // openModalImage();
} // }
return ( return (
<> <>
{/* MODAL KONFIRMASI */}
<Modal <Modal
opened={opened} opened={opened}
onClose={close} onClose={close}
@@ -115,6 +130,8 @@ function DetailDataPengaduan() {
</Stack> </Stack>
</Modal> </Modal>
{/* MODAL GAMBAR */}
<Modal <Modal
opened={openedModalImage} opened={openedModalImage}
onClose={closeModalImage} onClose={closeModalImage}
@@ -143,17 +160,27 @@ function DetailDataPengaduan() {
Pengaduan Pengaduan
</Title> </Title>
<Title order={4} c="dimmed"> <Title order={4} c="dimmed">
#PGf-2345-33 #{data?.noPengaduan}
</Title> </Title>
</Group> </Group>
<Badge <Badge
size="xl" size="xl"
variant="light" variant="light"
radius="sm" radius="sm"
color={"yellow"} color={
data?.status === "diterima"
? "green"
: data?.status === "ditolak"
? "red"
: data?.status === "selesai"
? "blue"
: data?.status === "dikerjakan"
? "purple"
: "yellow"
}
style={{ textTransform: "none" }} style={{ textTransform: "none" }}
> >
antrian {data?.status}
</Badge> </Badge>
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
@@ -166,7 +193,7 @@ function DetailDataPengaduan() {
<Text size="md">Judul</Text> <Text size="md">Judul</Text>
</Group> </Group>
<Text size="md" c={"white"}> <Text size="md" c={"white"}>
Judul Pengaduan {_.upperFirst(data?.title)}
</Text> </Text>
</Flex> </Flex>
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
@@ -175,7 +202,7 @@ function DetailDataPengaduan() {
<Text size="md">Lokasi</Text> <Text size="md">Lokasi</Text>
</Group> </Group>
<Text size="md" c="white"> <Text size="md" c="white">
fwef {_.upperFirst(data?.location)}
</Text> </Text>
</Flex> </Flex>
</Stack> </Stack>
@@ -188,7 +215,7 @@ function DetailDataPengaduan() {
<Text size="md">Kategori</Text> <Text size="md">Kategori</Text>
</Group> </Group>
<Text size="md" c="white"> <Text size="md" c="white">
fwef {_.upperFirst(data?.category)}
</Text> </Text>
</Flex> </Flex>
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
@@ -196,7 +223,7 @@ function DetailDataPengaduan() {
<IconPhotoScan size={20} /> <IconPhotoScan size={20} />
<Text size="md">Gambar</Text> <Text size="md">Gambar</Text>
</Group> </Group>
<Anchor href="#" onClick={handleLihatGambar}> <Anchor href="#" onClick={() => { }}>
Lihat Gambar Lihat Gambar
</Anchor> </Anchor>
</Flex> </Flex>
@@ -210,24 +237,22 @@ function DetailDataPengaduan() {
<Text size="md">Detail</Text> <Text size="md">Detail</Text>
</Group> </Group>
<Text size="md" c="white"> <Text size="md" c="white">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. {_.upperFirst(data?.detail)}
Illum, corporis iusto. Suscipit veritatis quas, non nobis
fuga, laudantium accusantium tempora sint aliquid architecto
totam esse eum excepturi nostrum fugiat ut.
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconInfoTriangle size={20} />
<Text size="md">Keterangan</Text>
</Group>
<Text size="md" c={"white"}>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At
fugiat eligendi nesciunt dolore? Maiores a cumque vitae
suscipit incidunt quos beatae modi, vel, id ullam quae
voluptas, deserunt quas placeat.
</Text> </Text>
</Flex> </Flex>
{
data?.keterangan && (
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconInfoTriangle size={20} />
<Text size="md">Keterangan</Text>
</Group>
<Text size="md" c={"white"}>
{_.upperFirst(data?.keterangan)}
</Text>
</Flex>
)
}
</Stack> </Stack>
</Grid.Col> </Grid.Col>
<Grid.Col span={12}> <Grid.Col span={12}>
@@ -259,23 +284,7 @@ function DetailDataPengaduan() {
); );
} }
function DetailDataHistori() { function DetailDataHistori({ data }: { data: any }) {
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
return ( return (
<Card <Card
radius="md" radius="md"
@@ -304,33 +313,25 @@ function DetailDataHistori() {
<Table.Th>User</Table.Th> <Table.Th>User</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody>{rows}</Table.Tbody> <Table.Tbody>
{
data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td>{item.createdAt}</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>{item.user}</Table.Td>
</Table.Tr>
))
}
</Table.Tbody>
</Table> </Table>
</Stack> </Stack>
</Card> </Card>
); );
} }
function DetailUserPengaduan() { function DetailUserPengaduan({ data }: { data: any }) {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
},
}),
);
useShallowEffect(() => {
mutate();
}, [status, value]);
const list = data?.data || [];
return ( return (
<Card <Card
radius="md" radius="md"
@@ -359,7 +360,7 @@ function DetailUserPengaduan() {
<Text size="md">Nama</Text> <Text size="md">Nama</Text>
</Group> </Group>
<Text size="md" c={"white"}> <Text size="md" c={"white"}>
Amalia Dwi Yustiani {data?.name}
</Text> </Text>
</Group> </Group>
<Group justify="space-between"> <Group justify="space-between">
@@ -368,7 +369,7 @@ function DetailUserPengaduan() {
<Text size="md">Telepon</Text> <Text size="md">Telepon</Text>
</Group> </Group>
<Text size="md" c="white"> <Text size="md" c="white">
08123456789 {data?.phone}
</Text> </Text>
</Group> </Group>
<Group justify="space-between"> <Group justify="space-between">
@@ -377,7 +378,7 @@ function DetailUserPengaduan() {
<Text size="md">Jumlah Pengaduan</Text> <Text size="md">Jumlah Pengaduan</Text>
</Group> </Group>
<Text size="md" c="white"> <Text size="md" c="white">
10 {data?.pengaduan}
</Text> </Text>
</Group> </Group>
<Group justify="space-between"> <Group justify="space-between">
@@ -386,7 +387,7 @@ function DetailUserPengaduan() {
<Text size="md">Jumlah Pelayanan Surat</Text> <Text size="md">Jumlah Pelayanan Surat</Text>
</Group> </Group>
<Text size="md" c="white"> <Text size="md" c="white">
10 {data?.pelayanan}
</Text> </Text>
</Group> </Group>
</Stack> </Stack>

View File

@@ -48,20 +48,24 @@ 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 { data, mutate, isLoading } = useSwr("/pengaduan/count", () =>
apiFetch.api.pengaduan.count.get().then((res) => res.data), apiFetch.api.pengaduan.count.get().then((res) => res.data),
); );
useShallowEffect(() => {
mutate();
}, []);
return ( return (
<Tabs defaultValue={status || "semua"} color="teal"> <Tabs defaultValue={status || "semua"} color="teal">
<Tabs.List grow> <Tabs.List grow>
<Tabs.Tab <Tabs.Tab
value="all" value="semua"
onClick={() => { onClick={() => {
navigate("?status=semua"); navigate("?status=semua");
}} }}
> >
Semua ({dataCount?.data?.semua || 0}) Semua ({data?.semua || 0})
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="antrian" value="antrian"
@@ -69,7 +73,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=antrian"); navigate("?status=antrian");
}} }}
> >
Antrian ({dataCount?.data?.antrian || 0}) Antrian ({data?.antrian || 0})
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="diterima" value="diterima"
@@ -77,7 +81,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=diterima"); navigate("?status=diterima");
}} }}
> >
Diterima ({dataCount?.data?.diterima || 0}) Diterima ({data?.diterima || 0})
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="dikerjakan" value="dikerjakan"
@@ -85,7 +89,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=dikerjakan"); navigate("?status=dikerjakan");
}} }}
> >
Dikerjakan ({dataCount?.data?.dikerjakan || 0}) Dikerjakan ({data?.dikerjakan || 0})
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="selesai" value="selesai"
@@ -93,7 +97,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=selesai"); navigate("?status=selesai");
}} }}
> >
Selesai ({dataCount?.data?.selesai || 0}) Selesai ({data?.selesai || 0})
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="ditolak" value="ditolak"
@@ -101,7 +105,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=ditolak"); navigate("?status=ditolak");
}} }}
> >
Ditolak ({dataCount?.data?.ditolak || 0}) Ditolak ({data?.ditolak || 0})
</Tabs.Tab> </Tabs.Tab>
</Tabs.List> </Tabs.List>
</Tabs> </Tabs>

View File

@@ -29,7 +29,7 @@ const PengaduanRoute = new Elysia({
}) })
return {data} return { data }
}, { }, {
detail: { detail: {
summary: "List Kategori Pengaduan", summary: "List Kategori Pengaduan",
@@ -316,12 +316,14 @@ Respon:
}) })
.get("/detail", async ({ query }) => { .get("/detail", async ({ query }) => {
const { id } = query const { id } = query
const data = await prisma.pengaduan.findUnique({
const data = await prisma.pengaduan.findFirst({
where: { where: {
id,
OR: [ OR: [
{ {
noPengaduan: id noPengaduan: id
}, {
id: id
} }
] ]
}, },
@@ -346,6 +348,13 @@ Respon:
Warga: { Warga: {
select: { select: {
name: true, name: true,
phone: true,
_count: {
select: {
Pengaduan: true,
PelayananAjuan: true,
}
}
} }
} }
} }
@@ -374,27 +383,37 @@ Respon:
id: item.id, id: item.id,
deskripsi: item.deskripsi, deskripsi: item.deskripsi,
status: item.status, status: item.status,
createdAt: item.createdAt, createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
idUser: item.idUser, idUser: item.idUser,
nameUser: item.User?.name, nameUser: item.User?.name,
} }
}) })
const datafix = { const warga = {
name: data?.Warga?.name,
phone: data?.Warga?.phone,
pengaduan: data?.Warga?._count.Pengaduan,
pelayanan: data?.Warga?._count.PelayananAjuan,
}
const dataPengaduan = {
id: data?.id, id: data?.id,
noPengaduan: data?.noPengaduan, noPengaduan: data?.noPengaduan,
title: data?.title, title: data?.title,
detail: data?.detail, detail: data?.detail,
location: data?.location, location: data?.location,
image: data?.image, image: data?.image,
CategoryPengaduan: data?.CategoryPengaduan.name, category: data?.CategoryPengaduan.name,
idWarga: data?.idWarga,
nameWarga: data?.Warga?.name,
status: data?.status, status: data?.status,
keterangan: data?.keterangan, keterangan: data?.keterangan,
createdAt: data?.createdAt, createdAt: data?.createdAt,
updatedAt: data?.updatedAt, updatedAt: data?.updatedAt,
}
const datafix = {
pengaduan: dataPengaduan,
history: dataHistoryFix, history: dataHistoryFix,
warga: warga,
} }
return datafix return datafix