Compare commits

...

6 Commits

Author SHA1 Message Date
3e09c934d4 fix : list pengaduan error 2025-11-17 17:25:14 +08:00
282b9678b3 upd: dashboard admin
Deskripsi:
- detail pengaduan
- menerima pengaduan
- mengerjakan pengaduan/
- menyelesaikan pengaduan
- menolak pengaduan

No Issues
2025-11-17 17:13:11 +08:00
ceed3e67c7 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
2025-11-17 13:42:32 +08:00
04b5d26507 upd : dahboard admin
Deskripsi:
- detail pengaduan

No Issues
2025-11-17 13:41:34 +08:00
327434b42e Merge pull request 'upd: warga route' (#27) from amalia/14-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/27
2025-11-14 17:27:15 +08:00
c4e4aaffe7 upd: warga route 2025-11-14 17:26:42 +08:00
4 changed files with 234 additions and 134 deletions

View File

@@ -33,6 +33,7 @@ const Api = new Elysia({
.use(PengaduanRoute)
.use(PelayananRoute)
.use(ConfigurationDesaRoute)
.use(WargaRoute)
.use(TestPengaduanRoute)
.use(apiAuth)
.use(ApiKeyRoute)

View File

@@ -1,3 +1,4 @@
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import {
Anchor,
@@ -25,10 +26,13 @@ import {
IconInfoTriangle,
IconMapPin,
IconMessageReport,
IconPhone,
IconPhotoScan,
IconUser,
} from "@tabler/icons-react";
import { useState } from "react";
import type { User } from "generated/prisma";
import _ from "lodash";
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import useSwr from "swr";
@@ -36,50 +40,95 @@ export default function DetailPengaduanPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.detail.get({
query: {
id: id!,
},
}),
);
useShallowEffect(() => {
mutate();
}, []);
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPengaduan />
<DetailDataHistori />
<DetailDataPengaduan data={data?.data?.pengaduan} onAction={() => { mutate(); }} />
<DetailDataHistori data={data?.data?.history} />
</Stack>
</Grid.Col>
<Grid.Col span={4}>
<DetailUserPengaduan />
<DetailUserPengaduan data={data?.data?.warga} />
</Grid.Col>
</Grid>
</Container>
);
}
function DetailDataPengaduan() {
function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => void }) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [openedModalImage, { open: openModalImage, close: closeModalImage }] =
useDisclosure(false);
const [keterangan, setKeterangan] = useState("");
const [host, setHost] = useState<User | null>(null);
async function handleLihatGambar() {
const res = await apiFetch.api.pengaduan.image.get({
query: {
fileName: "57d5ce89-7d18-4244-9f4c-ca21b70adb7e",
},
});
console.error("client", res);
// const blob = await res.data?.blob();
// setImageSrc(URL.createObjectURL(blob!));
// openModalImage();
useEffect(() => {
async function fetchHost() {
const { data } = await apiFetch.api.user.find.get();
setHost(data?.user ?? null);
}
fetchHost();
}, []);
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try {
const res = await apiFetch.api.pengaduan["update-status"].post({
id: data?.id,
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : data.status == 'diterima' ? 'dikerjakan' : 'selesai',
keterangan: keterangan,
idUser: host?.id ?? ""
});
if (res?.status === 200) {
onAction();
close();
notification({
title: "Success",
message: "Success update pengaduan",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to update pengaduan",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to update pengaduan",
type: "error",
});
}
}
return (
<>
{/* MODAL KONFIRMASI */}
<Modal
opened={opened}
onClose={close}
title={"Konfirmasi"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
@@ -89,25 +138,25 @@ function DetailDataPengaduan() {
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
</Text>
<Textarea size="md" minRows={5} />
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" color="red" onClick={close}>
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
Tolak
</Button>
</Group>
</>
) : (
<>
<Text>Anda yakin ingin menerima pengaduan ini?</Text>
<Text>Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : data.status == 'diterima' ? 'mengerjakan' : 'menyelesaikan'} pengaduan ini?</Text>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
Tidak
</Button>
<Button variant="filled" color="green" onClick={close}>
Terima
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")}>
Ya
</Button>
</Group>
</>
@@ -115,11 +164,12 @@ function DetailDataPengaduan() {
</Stack>
</Modal>
{/* MODAL GAMBAR */}
<Modal
opened={openedModalImage}
onClose={closeModalImage}
title="Gambar Pengaduan"
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Image src={imageSrc!} />
@@ -143,17 +193,27 @@ function DetailDataPengaduan() {
Pengaduan
</Title>
<Title order={4} c="dimmed">
#PGf-2345-33
#{data?.noPengaduan}
</Title>
</Group>
<Badge
size="xl"
variant="light"
radius="sm"
color={"yellow"}
color={
data?.status === "diterima"
? "green"
: data?.status === "ditolak"
? "red"
: data?.status === "selesai"
? "blue"
: data?.status === "dikerjakan"
? "gray"
: "yellow"
}
style={{ textTransform: "none" }}
>
antrian
{data?.status}
</Badge>
</Flex>
<Divider my={0} />
@@ -166,7 +226,7 @@ function DetailDataPengaduan() {
<Text size="md">Judul</Text>
</Group>
<Text size="md" c={"white"}>
Judul Pengaduan
{_.upperFirst(data?.title)}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
@@ -175,7 +235,7 @@ function DetailDataPengaduan() {
<Text size="md">Lokasi</Text>
</Group>
<Text size="md" c="white">
fwef
{_.upperFirst(data?.location)}
</Text>
</Flex>
</Stack>
@@ -188,7 +248,7 @@ function DetailDataPengaduan() {
<Text size="md">Kategori</Text>
</Group>
<Text size="md" c="white">
fwef
{_.upperFirst(data?.category)}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
@@ -196,7 +256,7 @@ function DetailDataPengaduan() {
<IconPhotoScan size={20} />
<Text size="md">Gambar</Text>
</Group>
<Anchor href="#" onClick={handleLihatGambar}>
<Anchor href="#" onClick={() => { }}>
Lihat Gambar
</Anchor>
</Flex>
@@ -210,47 +270,73 @@ function DetailDataPengaduan() {
<Text size="md">Detail</Text>
</Group>
<Text size="md" c="white">
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
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.
{_.upperFirst(data?.detail)}
</Text>
</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>
</Grid.Col>
<Grid.Col span={12}>
<Group justify="center" grow>
<Button
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
{
data?.status === "antrian" ? (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Kerjakan
</Button>
</Group>
) : data?.status === "dikerjakan" ? (
<Group justify="center" grow>
<Button
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Selesai
</Button>
</Group>
) : <></>
}
</Grid.Col>
</Grid>
</Stack>
@@ -259,23 +345,7 @@ function DetailDataPengaduan() {
);
}
function DetailDataHistori() {
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>
));
function DetailDataHistori({ data }: { data: any }) {
return (
<Card
radius="md"
@@ -304,33 +374,25 @@ function DetailDataHistori() {
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
<Table.Tbody>
{
data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>
</Table.Tr>
))
}
</Table.Tbody>
</Table>
</Stack>
</Card>
);
}
function DetailUserPengaduan() {
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 || [];
function DetailUserPengaduan({ data }: { data: any }) {
return (
<Card
radius="md"
@@ -359,16 +421,16 @@ function DetailUserPengaduan() {
<Text size="md">Nama</Text>
</Group>
<Text size="md" c={"white"}>
Amalia Dwi Yustiani
{data?.name}
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconMapPin size={20} />
<IconPhone size={20} />
<Text size="md">Telepon</Text>
</Group>
<Text size="md" c="white">
08123456789
{data?.phone}
</Text>
</Group>
<Group justify="space-between">
@@ -377,7 +439,7 @@ function DetailUserPengaduan() {
<Text size="md">Jumlah Pengaduan</Text>
</Group>
<Text size="md" c="white">
10
{data?.pengaduan}
</Text>
</Group>
<Group justify="space-between">
@@ -386,7 +448,7 @@ function DetailUserPengaduan() {
<Text size="md">Jumlah Pelayanan Surat</Text>
</Group>
<Text size="md" c="white">
10
{data?.pelayanan}
</Text>
</Group>
</Stack>

View File

@@ -24,7 +24,7 @@ import {
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
import { proxy } from "valtio";
import { proxy, subscribe } from "valtio";
const state = proxy({ reload: "" });
function reloadState() {
@@ -48,20 +48,24 @@ export default function PengaduanListPage() {
function TabListPengaduan({ status }: { status: string }) {
const navigate = useNavigate();
const dataCount = useSwr("/pengaduan/count", () =>
const { data, mutate, isLoading } = useSwr("/pengaduan/count", () =>
apiFetch.api.pengaduan.count.get().then((res) => res.data),
);
useShallowEffect(() => {
mutate();
}, []);
return (
<Tabs defaultValue={status || "semua"} color="teal">
<Tabs.List grow>
<Tabs.Tab
value="all"
value="semua"
onClick={() => {
navigate("?status=semua");
}}
>
Semua ({dataCount?.data?.semua || 0})
Semua ({data?.semua || 0})
</Tabs.Tab>
<Tabs.Tab
value="antrian"
@@ -69,7 +73,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=antrian");
}}
>
Antrian ({dataCount?.data?.antrian || 0})
Antrian ({data?.antrian || 0})
</Tabs.Tab>
<Tabs.Tab
value="diterima"
@@ -77,7 +81,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=diterima");
}}
>
Diterima ({dataCount?.data?.diterima || 0})
Diterima ({data?.diterima || 0})
</Tabs.Tab>
<Tabs.Tab
value="dikerjakan"
@@ -85,7 +89,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=dikerjakan");
}}
>
Dikerjakan ({dataCount?.data?.dikerjakan || 0})
Dikerjakan ({data?.dikerjakan || 0})
</Tabs.Tab>
<Tabs.Tab
value="selesai"
@@ -93,7 +97,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=selesai");
}}
>
Selesai ({dataCount?.data?.selesai || 0})
Selesai ({data?.selesai || 0})
</Tabs.Tab>
<Tabs.Tab
value="ditolak"
@@ -101,7 +105,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=ditolak");
}}
>
Ditolak ({dataCount?.data?.ditolak || 0})
Ditolak ({data?.ditolak || 0})
</Tabs.Tab>
</Tabs.List>
</Tabs>
@@ -120,21 +124,28 @@ function ListPengaduan({ status }: { status: StatusKey }) {
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
const { data, mutate, isLoading } = useSwr("/", async () => {
const res = await apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
},
}),
);
});
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
});
useShallowEffect(() => {
mutate();
}, [status, value]);
useShallowEffect(() => {
const unsubscribe = subscribe(state, () => mutate());
return () => unsubscribe();
}, []);
if (isLoading)
return (
<Card
@@ -152,7 +163,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Card>
);
const list = data?.data || [];
const list = data || [];
return (
<Stack gap="xl">
@@ -186,7 +197,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
list?.map((v: any) => (
Array.isArray(list) && list?.map((v: any) => (
<Card
key={v.id}
radius="lg"
@@ -229,7 +240,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
: v.status === "selesai"
? "blue"
: v.status === "dikerjakan"
? "purple"
? "gray"
: "yellow"
}
style={{ textTransform: "none" }}

View File

@@ -28,8 +28,8 @@ const PengaduanRoute = new Elysia({
}
})
return {data}
return { data }
}, {
detail: {
summary: "List Kategori Pengaduan",
@@ -316,12 +316,14 @@ Respon:
})
.get("/detail", async ({ query }) => {
const { id } = query
const data = await prisma.pengaduan.findUnique({
const data = await prisma.pengaduan.findFirst({
where: {
id,
OR: [
{
noPengaduan: id
}, {
id: id
}
]
},
@@ -346,6 +348,13 @@ Respon:
Warga: {
select: {
name: true,
phone: true,
_count: {
select: {
Pengaduan: true,
PelayananAjuan: true,
}
}
}
}
}
@@ -374,27 +383,44 @@ Respon:
id: item.id,
deskripsi: item.deskripsi,
status: item.status,
createdAt: item.createdAt,
createdAt: item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
}),
idUser: item.idUser,
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,
noPengaduan: data?.noPengaduan,
title: data?.title,
detail: data?.detail,
location: data?.location,
image: data?.image,
CategoryPengaduan: data?.CategoryPengaduan.name,
idWarga: data?.idWarga,
nameWarga: data?.Warga?.name,
category: data?.CategoryPengaduan.name,
status: data?.status,
keterangan: data?.keterangan,
createdAt: data?.createdAt,
updatedAt: data?.updatedAt,
}
const datafix = {
pengaduan: dataPengaduan,
history: dataHistoryFix,
warga: warga,
}
return datafix