Files
jenna-mcp/src/pages/scr/dashboard/pengaduan/detail_page.tsx
amaliadwiy 82765f6ef0 fix: qc
Deskripsi:
- breadcumbs
- back
- active menu

nb: blm selesai

No Issues
2026-01-12 17:43:04 +08:00

591 lines
17 KiB
TypeScript

import BreadCrumbs from "@/components/BreadCrumbs";
import ModalFile from "@/components/ModalFile";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import {
Anchor,
Badge,
Button,
Card,
Container,
Divider,
Flex,
Grid,
Group,
Modal,
Spoiler,
Stack,
Table,
Text,
Textarea,
Title,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCategory,
IconFileCertificate,
IconInfoTriangle,
IconMapPin,
IconMessageReport,
IconPhone,
IconPhotoScan,
IconUser,
} from "@tabler/icons-react";
import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash";
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import useSwr from "swr";
export default function DetailPengaduanPage() {
const dataMenu = [
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
{ title: "Pengaduan", link: "/scr/dashboard/pengaduan/list", active: false },
{ title: "Detail Pengaduan", link: "#", active: true },
];
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={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPengaduan
data={data?.data?.pengaduan}
phone={data && data.data && data.data.warga ? data.data.warga.phone : null}
onAction={() => {
mutate();
}}
/>
<DetailDataHistori data={data?.data?.history} />
</Stack>
</Grid.Col>
<Grid.Col span={4}>
<DetailUserPengaduan data={data?.data?.warga} />
</Grid.Col>
</Grid>
</Container>
);
}
function DetailDataPengaduan({
data,
phone,
onAction,
}: {
data: any | null;
phone?: string | null;
onAction: () => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [openedPreview, setOpenedPreview] = useState(false);
const [keterangan, setKeterangan] = useState("");
const [host, setHost] = useState<User | null>(null);
const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
async function fetchHost() {
const { data } = await apiFetch.api.user.find.get();
setHost(data?.user ?? null);
if (data?.permissions && Array.isArray(data.permissions)) {
const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("pengaduan"),
);
setPermissions(onlySetting);
}
}
fetchHost();
}, []);
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try {
setIsLoading(true);
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) {
const resWA = await apiFetch.api["send-wa"].pengaduan.post({
noPengaduan: data?.noPengaduan,
judulPengaduan: data?.title,
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: data.status == "diterima"
? "dikerjakan"
: "selesai",
alasan: keterangan,
tlp: String(phone),
})
onAction();
close();
notification({
title: "Success",
message: "Success update pengaduan",
type: "success",
});
if (resWA?.status === 200) {
if (resWA.data?.success) {
notification({
title: "Success",
message: "Success send message to warga",
type: "success",
});
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} 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",
});
} finally {
setIsLoading(false);
}
};
return (
<>
{/* MODAL KONFIRMASI */}
<Modal
opened={opened}
onClose={close}
title={"Konfirmasi"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
{catModal === "tolak" ? (
<>
<Text>
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
</Text>
<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"
disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")}
loading={isLoading}
>
Tolak
</Button>
</Group>
</>
) : (
<>
<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}>
Tidak
</Button>
<Button
variant="filled"
color="green"
onClick={() => handleKonfirmasi("terima")}
loading={isLoading}
>
Ya
</Button>
</Group>
</>
)}
</Stack>
</Modal>
{/* MODAL GAMBAR */}
<ModalFile
open={openedPreview && !_.isEmpty(data?.image)}
onClose={() => setOpenedPreview(false)}
folder="pengaduan"
fileName={data?.image}
/>
<Card
radius="md"
p="lg"
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">
<Group gap="xs">
<Title order={4} c="gray.2">
Pengaduan
</Title>
<Title order={4} c="dimmed">
#{data?.noPengaduan}
</Title>
</Group>
<Badge
size="xl"
variant="light"
radius="sm"
color={
data?.status === "diterima"
? "green"
: data?.status === "ditolak"
? "red"
: data?.status === "selesai"
? "blue"
: data?.status === "dikerjakan"
? "gray"
: "yellow"
}
style={{ textTransform: "none" }}
>
{data?.status}
</Badge>
</Flex>
<Divider my={0} />
<Grid>
<Grid.Col span={6}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
<Text size="md">Judul</Text>
</Group>
<Text size="md" c={"white"}>
{_.upperFirst(data?.title)}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconMapPin size={20} />
<Text size="md">Lokasi</Text>
</Group>
<Text size="md" c="white">
{_.upperFirst(data?.location)}
</Text>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={6}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconCategory size={20} />
<Text size="md">Kategori</Text>
</Group>
<Text size="md" c="white">
{_.upperFirst(data?.category)}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconPhotoScan size={20} />
<Text size="md">Gambar</Text>
</Group>
{data?.image != null && data?.image != "" ? (
<Anchor
href="#"
onClick={() => {
setOpenedPreview(true);
}}
>
Lihat Gambar
</Anchor>
) : (
<Text size="md" c="white">
-
</Text>
)}
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={12}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
<Text size="md">Detail</Text>
</Group>
<Text size="md" c="white">
{_.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}>
{data?.status === "antrian" ? (
<Group justify="center" grow>
<Button
variant="light"
disabled={!permissions.includes("pengaduan.antrian.tolak")}
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
disabled={!permissions.includes("pengaduan.antrian.terima")}
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
variant="filled"
disabled={
!permissions.includes("pengaduan.diterima.dikerjakan")
}
onClick={() => {
setCatModal("terima");
open();
}}
>
Kerjakan
</Button>
</Group>
) : data?.status === "dikerjakan" ? (
<Group justify="center" grow>
<Button
variant="filled"
disabled={
!permissions.includes("pengaduan.dikerjakan.selesai")
}
onClick={() => {
setCatModal("terima");
open();
}}
>
Selesai
</Button>
</Group>
) : (
<></>
)}
</Grid.Col>
</Grid>
</Stack>
</Card>
</>
);
}
function DetailDataHistori({ data }: { data: any }) {
return (
<Card
radius="md"
p="lg"
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">
<Title order={4} c="gray.2">
Riwayat Pengaduan
</Title>
</Flex>
<Divider my={0} />
<Spoiler
maxHeight={200}
showLabel="Show more"
hideLabel="Hide"
transitionDuration={1000}
>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</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>
</Spoiler>
</Stack>
</Card>
);
}
function DetailUserPengaduan({ data }: { data: any }) {
return (
<Card
radius="md"
p="lg"
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={4} c="gray.2">
Warga
</Title>
</Flex>
</Flex>
<Divider my={0} />
<Stack gap="md">
<Group justify="space-between">
<Group gap="xs">
<IconUser size={20} />
<Text size="md">Nama</Text>
</Group>
<Text size="md" c={"white"}>
{data?.name}
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconPhone size={20} />
<Text size="md">Telepon</Text>
</Group>
<Text size="md" c="white">
{data?.phone}
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconMessageReport size={20} />
<Text size="md">Jumlah Pengaduan</Text>
</Group>
<Text size="md" c="white">
{data?.pengaduan}
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconFileCertificate size={20} />
<Text size="md">Jumlah Pelayanan Surat</Text>
</Group>
<Text size="md" c="white">
{data?.pelayanan}
</Text>
</Group>
</Stack>
</Stack>
</Card>
);
}