dashboard admin

Deskripsi:
- list pelayanan surat
- detail pelayanan surat
- terima pelayanan surat
- menolak pelayanan surat
- menyelesaikan pelayanan surat
- tambah surat > databse

No Issues
This commit is contained in:
2025-11-18 17:37:07 +08:00
parent 083eb11bb0
commit 79660a766c
6 changed files with 670 additions and 215 deletions

View File

@@ -1,6 +1,6 @@
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import {
Anchor,
Badge,
Button,
Card,
@@ -9,89 +9,160 @@ import {
Flex,
Grid,
Group,
List,
Modal,
Stack,
Table,
Text,
Textarea,
Title,
ThemeIcon,
Title
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCategory,
IconCheck,
IconFileCertificate,
IconInfoTriangle,
IconMapPin,
IconFileCheck,
IconMessageReport,
IconPhotoScan,
IconUser,
IconPhone,
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";
export default function DetailPelayananPage() {
export default function DetailPengajuanPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pelayanan.detail.get({
query: {
id: id!,
},
}),
);
useShallowEffect(() => {
mutate();
}, []);
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPelayanan />
<DetailDataHistori />
<DetailDataPengajuan data={data?.data?.pengajuan} syaratDokumen={data?.data?.syaratDokumen} dataText={data?.data?.dataText} onAction={() => { mutate(); }} />
<DetailDataHistori data={data?.data?.history} />
</Stack>
</Grid.Col>
<Grid.Col span={4}>
<DetailUserPelayanan />
<DetailUserPengajuan data={data?.data?.warga} />
</Grid.Col>
</Grid>
</Container>
);
}
function DetailDataPelayanan() {
function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data: any, syaratDokumen: any, dataText: any, onAction: () => void }) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [keterangan, setKeterangan] = useState("");
const [host, setHost] = useState<User | null>(null);
const [noSurat, setNoSurat] = useState("");
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.pelayanan["update-status"].post({
id: data?.id,
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : 'selesai',
keterangan: keterangan,
idUser: host?.id ?? "",
noSurat: noSurat
});
if (res?.status === 200) {
onAction();
close();
notification({
title: "Success",
message: "Success update pengajuan surat",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to update pengajuan surat",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to update pengajuan surat",
type: "error",
});
}
}
return (
<>
{/* MODAL KONFIRMASI */}
<Modal
opened={opened}
onClose={close}
title={"Konfirmasi"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
{catModal === "tolak" ? (
<>
<Text>
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
Anda yakin ingin menolak pengajuan surat 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' : 'menyetujui'} pengajuan surat ini?
{
data.status == 'diterima' && 'Masukkan nomer surat yang akan dibuat'
}
</Text>
{
data.status == 'diterima' && (
<Textarea size="md" minRows={5} value={noSurat} onChange={(e) => setNoSurat(e.target.value)} placeholder="Contoh : 08/D-IV/11/2025" />
)
}
<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")} disabled={data.status == 'diterima' && noSurat.length < 1}>
Ya
</Button>
</Group>
</>
@@ -114,117 +185,141 @@ function DetailDataPelayanan() {
<Flex align="center" justify="space-between">
<Group gap="xs">
<Title order={4} c="gray.2">
Pelayanan Surat
Pengajuan {data?.category}
</Title>
<Title order={4} c="dimmed">
#PGf-2345-33
#{data?.noPengajuan}
</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} />
<Grid>
<Grid.Col span={6}>
<Stack gap="md">
<Grid.Col span={12}>
<Stack gap="lg">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconFileCheck size={20} />
<Text size="md">Syarat Dokumen</Text>
</Group>
<List
spacing="sm"
pt={10}
icon={
<ThemeIcon color="green" size={20} radius="xl">
<IconCheck size={13} />
</ThemeIcon>
}
>
{syaratDokumen?.map((v: any) => (
<List.Item key={v.id}>{v.jenis}</List.Item>
))}
</List>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
<Text size="md">Judul</Text>
<Text size="md">Data Pelengkap</Text>
</Group>
<Text size="md" c={"white"}>
Judul Pelayanan Surat
</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">
fwef
</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">
fwef
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconPhotoScan size={20} />
<Text size="md">Gambar</Text>
</Group>
<Anchor href="https://mantine.dev/" target="_blank">
Lihat Gambar
</Anchor>
<Table withRowBorders={false}>
<Table.Tbody>
{
dataText?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap", width: "10%" }}>{_.upperFirst(item.jenis)}</Table.Td>
<Table.Td>:</Table.Td>
<Table.Td style={{ width: "85%" }}>{_.upperFirst(item.value)}</Table.Td>
</Table.Tr>
))
}
</Table.Tbody>
</Table>
</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>
{
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>
<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>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Setujui
</Button>
</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>
</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>
) : (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => { }}
>
Lihat Surat
</Button>
<Button
variant="light"
onClick={() => { }}
>
Download
</Button>
</Group>
)
}
</Grid.Col>
</Grid>
</Stack>
@@ -233,23 +328,7 @@ function DetailDataPelayanan() {
);
}
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"
@@ -265,7 +344,7 @@ function DetailDataHistori() {
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengaduan
Histori Pengajuan Surat
</Title>
</Flex>
<Divider my={0} />
@@ -278,33 +357,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 DetailUserPelayanan() {
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 DetailUserPengajuan({ data }: { data: any }) {
return (
<Card
radius="md"
@@ -333,16 +404,16 @@ function DetailUserPelayanan() {
<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">
@@ -351,7 +422,7 @@ function DetailUserPelayanan() {
<Text size="md">Jumlah Pengaduan</Text>
</Group>
<Text size="md" c="white">
10
{data?.pengaduan}
</Text>
</Group>
<Group justify="space-between">
@@ -360,7 +431,7 @@ function DetailUserPelayanan() {
<Text size="md">Jumlah Pelayanan Surat</Text>
</Group>
<Text size="md" c="white">
10
{data?.pelayanan}
</Text>
</Group>
</Stack>

View File

@@ -15,16 +15,15 @@ import {
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconClockHour3,
IconFileSad,
IconMapPin,
IconSearch,
IconUser
} from "@tabler/icons-react";
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() {
@@ -49,14 +48,14 @@ export default function PelayananSuratListPage() {
function TabListPelayananSurat({ status }: { status: string }) {
const navigate = useNavigate();
const dataCount = useSwr("/pelayanan-surat/count", () =>
apiFetch.api.pengaduan.count.get().then((res) => res.data),
apiFetch.api.pelayanan.count.get().then((res) => res.data),
);
return (
<Tabs defaultValue={status || "semua"} color="teal">
<Tabs.List grow>
<Tabs.Tab
value="all"
value="semua"
onClick={() => {
navigate("?status=semua");
}}
@@ -118,21 +117,29 @@ type StatusKey =
function ListPelayananSurat({ status }: { status: StatusKey }) {
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.pelayanan.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();
}, []);
const navigate = useNavigate();
if (isLoading)
@@ -147,19 +154,19 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
}}
>
<Text size="sm" c="dimmed">
Loading pengaduan...
Loading pelayanan surat...
</Text>
</Card>
);
const list = data?.data || [];
const list = data || [];
return (
<Stack gap="xl">
<Group grow>
<Input
value={value}
placeholder="Cari pengaduan..."
placeholder="Cari pengajuan..."
onChange={(event) => setValue(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
@@ -204,11 +211,11 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
<Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={3} c="gray.2">
{v.title}
{v.category}
</Title>
<Group>
<Title order={6} c="gray.5">
#{v.noPengaduan}
#{v.noPengajuan}
</Title>
<Text size="sm" c="dimmed">
{v.updatedAt}
@@ -227,7 +234,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
: v.status === "selesai"
? "blue"
: v.status === "dikerjakan"
? "purple"
? "gray"
: "yellow"
}
style={{ textTransform: "none" }}
@@ -241,28 +248,19 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
<Group gap="xs">
<IconClockHour3 size={20} color="white" />
<Text size="md" c="white">
Tanggal Aduan
Tanggal Ajuan
</Text>
</Group>
<Text size="md">{v.createdAt}</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconMapPin size={20} color="white" />
<IconUser size={20} color="white" />
<Text size="md" c="white">
Lokasi
Warga
</Text>
</Group>
<Text size="md">{v.location}</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} color="white" />
<Text size="md" c="white">
Detail
</Text>
</Group>
<Text size="md">{v.detail}</Text>
<Text size="md">{v.warga}</Text>
</Flex>
</Stack>
</Stack>