upd: form surat

Deskripsi:
- api detail categori list
- form awal buat surat

No Issues
This commit is contained in:
2025-12-16 17:38:13 +08:00
parent a13e51a724
commit 7c6e4ac9eb
38 changed files with 4236 additions and 2941 deletions

View File

@@ -28,16 +28,19 @@ export default function Login() {
window.location.href = clientRoutes["/scr/dashboard/warga/list-warga"];
break;
case "credential":
window.location.href = clientRoutes["/scr/dashboard/credential/credential"];
window.location.href =
clientRoutes["/scr/dashboard/credential/credential"];
break;
case "setting":
window.location.href = clientRoutes["/scr/dashboard/setting/detail-setting"];
window.location.href =
clientRoutes["/scr/dashboard/setting/detail-setting"];
break;
case "api_key":
window.location.href = clientRoutes["/scr/dashboard/apikey/apikey"];
break;
case "pelayanan":
window.location.href = clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
window.location.href =
clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
break;
default:
window.location.href = clientRoutes["/scr/dashboard"];

View File

@@ -0,0 +1,331 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Badge,
Box,
Button,
Card,
Container,
Divider,
Grid,
Group,
Select,
Stack,
Text,
TextInput,
Textarea,
Tooltip
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import {
IconBuildingCommunity,
IconInfoCircle,
IconMapPin,
IconUser
} from "@tabler/icons-react";
import React from "react";
import useSWR from "swr";
// =========================
// Reusable UI components
// =========================
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
return (
<Group justify="apart" gap="xs" align="center">
<Text fw={600}>{label}</Text>
{hint && (
<Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle">
<IconInfoCircle size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
);
}
function FormSection({
title,
icon,
children,
description,
}: {
title: string;
icon?: React.ReactNode;
children: React.ReactNode;
description?: string;
}) {
return (
<Card radius="md" shadow="sm" withBorder>
<Group justify="apart" align="center" mb="xs">
<Group align="center" gap="xs">
{icon}
<Text fw={700}>{title}</Text>
</Group>
{description && <Badge variant="light">{description}</Badge>}
</Group>
<Divider mb="sm" />
<Stack gap="sm">{children}</Stack>
</Card>
);
}
// =========================
// Main form component
// =========================
export default function FormSurat() {
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
apiFetch.api.pelayanan.category.get(),
);
const listCategory = data?.data || [];
useShallowEffect(() => {
mutate();
}, []);
return (
<Container size="md" w={"100%"}>
<Box>
<Stack gap="lg">
<Group justify="apart" align="center">
<Group align="center">
<IconBuildingCommunity size={28} />
<div>
<Text fw={800} size="xl">
Surat Keterangan Tidak Mampu (SKTM)
</Text>
<Text size="sm" c="dimmed">
Blangko resmi untuk pengajuan Surat Keterangan Tidak Mampu
digunakan untuk keperluan pendidikan, kesehatan, atau
administrasi.
</Text>
</div>
</Group>
<Group>
<Badge radius="sm">Form Length: 3 Sections</Badge>
</Group>
</Group>
<form>
<Stack gap="lg">
{/* Header Section */}
<FormSection
title="Pemohon"
icon={<IconUser size={16} />}
description="Informasi identitas pemohon"
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nama Lengkap"
hint="Nama lengkap pemohon"
/>
}
placeholder="Budi Setiawan"
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/>
}
placeholder="08123456789"
/>
</Grid.Col>
<Grid.Col span={12}>
<Select
label={<FieldLabel label="Jenis Surat" hint="Jenis surat yang ingin diajukan" />}
placeholder="Pilih jenis surat"
data={listCategory.map((item: any) => ({
value: item.id,
label: item.name,
}))}
/>
</Grid.Col>
</Grid>
</FormSection>
<FormSection
title="Syarat Dokumen"
description="Syarat dokumen yang diperlukan"
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nama Lengkap"
hint="Sesuai KTP"
/>
}
placeholder="Nama lengkap"
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="NIK"
hint="16 digit, tanpa spasi"
/>
}
placeholder="3201xxxxxxxxxxxx"
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Tempat, Tanggal Lahir"
hint="Contoh: Denpasar, 31-12-1990"
/>
}
placeholder="Tempat, tanggal lahir"
/>
</Grid.Col>
<Grid.Col span={6}>
<Select
label={<FieldLabel label="Jenis Kelamin" />}
placeholder="Pilih jenis kelamin"
data={["Laki-laki", "Perempuan"]}
/>
</Grid.Col>
<Grid.Col span={6}>
<Select
label={<FieldLabel label="Agama" />}
placeholder="Pilih agama"
data={[
"Islam",
"Kristen",
"Katolik",
"Hindu",
"Buddha",
"Konghucu",
"Lainnya",
]}
/>
</Grid.Col>
<Grid.Col span={6}>
<Select
label={<FieldLabel label="Status Perkawinan" />}
placeholder="Pilih status"
data={[
"Belum Kawin",
"Kawin",
"Cerai Hidup",
"Cerai Mati",
]}
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={<FieldLabel label="Pekerjaan" />}
placeholder="Pekerjaan"
/>
</Grid.Col>
<Grid.Col span={12}>
<Textarea
label={<FieldLabel label="Alamat Lengkap" />}
placeholder="Alamat domisili"
minRows={2}
/>
</Grid.Col>
<Grid.Col span={2}>
<TextInput
label={<FieldLabel label="RT" />}
placeholder="001"
/>
</Grid.Col>
<Grid.Col span={2}>
<TextInput
label={<FieldLabel label="RW" />}
placeholder="002"
/>
</Grid.Col>
<Grid.Col span={4}>
<TextInput
label={<FieldLabel label="Desa / Kelurahan" />}
placeholder="Desa"
/>
</Grid.Col>
<Grid.Col span={4}>
<TextInput
label={<FieldLabel label="Kecamatan" />}
placeholder="Kecamatan"
/>
</Grid.Col>
<Grid.Col span={4}>
<TextInput
label={<FieldLabel label="Kabupaten / Kota" />}
placeholder="Kabupaten / Kota"
/>
</Grid.Col>
</Grid>
</FormSection>
{/* Keterangan Section */}
<FormSection
title="Keterangan"
icon={<IconMapPin size={18} />}
description="Isi pernyataan SKTM"
>
<Textarea
label={
<FieldLabel
label="Isi Surat"
hint="Jelaskan kondisi ekonomi secara singkat"
/>
}
placeholder="Pernyataan resmi bahwa yang bersangkutan benar-benar tergolong keluarga tidak mampu..."
minRows={4}
/>
<TextInput
label={
<FieldLabel
label="Keperluan"
hint="Contoh: Beasiswa pendidikan / Perawatan kesehatan"
/>
}
placeholder="Keperluan surat"
/>
</FormSection>
{/* Actions */}
<Group justify="right" mt="md">
<Button variant="default" onClick={() => { }}>
Reset
</Button>
<Button type="submit">Kirim / Simpan</Button>
</Group>
</Stack>
</form>
</Stack>
</Box>
</Container>
);
}

View File

@@ -9,7 +9,7 @@ import {
Progress,
Stack,
Text,
Title
Title,
} from "@mantine/core";
export default function Dashboard() {

View File

@@ -285,45 +285,47 @@ function NavigationDashboard() {
return (
<Stack gap="xs" p="sm">
{navItems.filter((item) => permissions.includes(item.key)).map((item) => (
<NavLink
key={item.path}
active={isActive(item.path as keyof typeof clientRoute)}
leftSection={item.icon}
label={
<Flex align="center" gap={6}>
<Text fw={500}>{item.label}</Text>
{isActive(item.path as keyof typeof clientRoute) && (
<Badge
variant="light"
color="teal"
radius="sm"
size="xs"
style={{ textTransform: "none" }}
>
Active
</Badge>
)}
</Flex>
}
description={item.description}
onClick={() =>
navigate(clientRoutes[item.path as keyof typeof clientRoute])
}
style={{
backgroundColor: isActive(item.path as keyof typeof clientRoute)
? "rgba(0,255,200,0.1)"
: "transparent",
borderRadius: "8px",
transition: "all 0.2s ease",
}}
styles={{
label: { color: "white" },
description: { color: "#aaa" },
section: { color: "teal" },
}}
/>
))}
{navItems
.filter((item) => permissions.includes(item.key))
.map((item) => (
<NavLink
key={item.path}
active={isActive(item.path as keyof typeof clientRoute)}
leftSection={item.icon}
label={
<Flex align="center" gap={6}>
<Text fw={500}>{item.label}</Text>
{isActive(item.path as keyof typeof clientRoute) && (
<Badge
variant="light"
color="teal"
radius="sm"
size="xs"
style={{ textTransform: "none" }}
>
Active
</Badge>
)}
</Flex>
}
description={item.description}
onClick={() =>
navigate(clientRoutes[item.path as keyof typeof clientRoute])
}
style={{
backgroundColor: isActive(item.path as keyof typeof clientRoute)
? "rgba(0,255,200,0.1)"
: "transparent",
borderRadius: "8px",
transition: "all 0.2s ease",
}}
styles={{
label: { color: "white" },
description: { color: "#aaa" },
section: { color: "teal" },
}}
/>
))}
</Stack>
);
}

View File

@@ -19,7 +19,7 @@ import {
Text,
Textarea,
ThemeIcon,
Title
Title,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
@@ -29,7 +29,7 @@ import {
IconFileCheck,
IconMessageReport,
IconPhone,
IconUser
IconUser,
} from "@tabler/icons-react";
import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library";
@@ -59,7 +59,14 @@ export default function DetailPengajuanPage() {
<Grid>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPengajuan data={data?.data?.pengajuan} syaratDokumen={data?.data?.syaratDokumen} dataText={data?.data?.dataText} onAction={() => { mutate(); }} />
<DetailDataPengajuan
data={data?.data?.pengajuan}
syaratDokumen={data?.data?.syaratDokumen}
dataText={data?.data?.dataText}
onAction={() => {
mutate();
}}
/>
<DetailDataHistori data={data?.data?.history} />
</Stack>
</Grid.Col>
@@ -71,7 +78,17 @@ export default function DetailPengajuanPage() {
);
}
function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data: any, syaratDokumen: any, dataText: any, onAction: () => void }) {
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("");
@@ -88,7 +105,9 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
setHost(data?.user ?? null);
if (data?.permissions && Array.isArray(data.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("pelayanan"));
const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("pelayanan"),
);
setPermissions(onlySetting);
}
}
@@ -99,10 +118,15 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
try {
const res = await apiFetch.api.pelayanan["update-status"].post({
id: data?.id,
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : 'selesai',
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: "selesai",
keterangan: keterangan,
idUser: host?.id ?? "",
noSurat: noSurat
noSurat: noSurat,
});
if (res?.status === 200) {
@@ -120,7 +144,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
@@ -129,7 +152,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
type: "error",
});
}
}
};
useShallowEffect(() => {
if (viewImg) {
@@ -142,7 +165,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)}
onClose={() => {
setOpenedPreviewFile(false)
setOpenedPreviewFile(false);
}}
folder="syarat-dokumen"
fileName={viewImg}
@@ -159,14 +182,25 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
{catModal === "tolak" ? (
<>
<Text>
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan penolakan
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan
penolakan
</Text>
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
<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")}>
<Button
variant="filled"
color="red"
disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")}
>
Tolak
</Button>
</Group>
@@ -174,21 +208,31 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
) : (
<>
<Text>
Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : 'menyetujui'} pengajuan surat ini?
{
data.status == 'diterima' && 'Masukkan nomer surat yang akan dibuat'
}
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" />
)
}
{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}>
Tidak
</Button>
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")} disabled={data.status == 'diterima' && noSurat.length < 1}>
<Button
variant="filled"
color="green"
onClick={() => handleKonfirmasi("terima")}
disabled={data.status == "diterima" && noSurat.length < 1}
>
Ya
</Button>
</Group>
@@ -196,11 +240,13 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
)}
</Stack>
</Modal>
{
data?.status == "selesai" &&
(<ModalSurat open={openedPreview} onClose={() => setOpenedPreview(false)} surat={data?.idSurat} />)
}
{data?.status == "selesai" && (
<ModalSurat
open={openedPreview}
onClose={() => setOpenedPreview(false)}
surat={data?.idSurat}
/>
)}
<Card
radius="md"
@@ -263,7 +309,11 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
>
{syaratDokumen?.map((v: any) => (
<List.Item key={v.id}>
<Anchor onClick={() => { setViewImg(v.value) }}>
<Anchor
onClick={() => {
setViewImg(v.value);
}}
>
{v.jenis}
</Anchor>
</List.Item>
@@ -271,8 +321,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
</List>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
@@ -281,82 +329,85 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
<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>
))
}
{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}>
{
data?.status === "antrian" ? (
<Group justify="center" grow>
<Button
disabled={!permissions.includes("pelayanan.antrian.tolak")}
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
disabled={!permissions.includes("pelayanan.antrian.terima")}
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
disabled={!permissions.includes("pelayanan.diterima.tolak")}
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
disabled={!permissions.includes("pelayanan.diterima.setujui")}
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Setujui
</Button>
</Group>
) :
data?.status === "selesai" ?
(
<Group justify="center" grow>
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
>
Surat
</Button>
</Group>
)
: <></>
}
{data?.status === "antrian" ? (
<Group justify="center" grow>
<Button
disabled={!permissions.includes("pelayanan.antrian.tolak")}
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
disabled={!permissions.includes("pelayanan.antrian.terima")}
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
disabled={!permissions.includes("pelayanan.diterima.tolak")}
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
disabled={
!permissions.includes("pelayanan.diterima.setujui")
}
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Setujui
</Button>
</Group>
) : data?.status === "selesai" ? (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
>
Surat
</Button>
</Group>
) : (
<></>
)}
</Grid.Col>
</Grid>
</Stack>
@@ -395,26 +446,25 @@ function DetailDataHistori({ data }: { data: any }) {
</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>
))
}
{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>
</Stack>

View File

@@ -20,7 +20,7 @@ import {
IconClockHour3,
IconFileSad,
IconSearch,
IconUser
IconUser,
} from "@tabler/icons-react";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
@@ -123,7 +123,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
take: "",
page: page.toString(),
},
})
}),
);
useShallowEffect(() => {
@@ -131,12 +131,10 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
mutate();
}, [status, value]);
useShallowEffect(() => {
mutate();
}, [page]);
useShallowEffect(() => {
const unsubscribe = subscribe(state, () => mutate());
return () => unsubscribe();
@@ -189,8 +187,16 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Grid.Col>
<Grid.Col span={3}>
<Group justify="flex-end">
<Text size="sm" c="gray.5">{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination total={totalPage} value={page} onChange={setPage} withPages={false} />
<Text
size="sm"
c="gray.5"
>{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination
total={totalPage}
value={page}
onChange={setPage}
withPages={false}
/>
</Group>
</Grid.Col>
</Grid>
@@ -204,7 +210,8 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
Array.isArray(list) && list?.map((v: any) => (
Array.isArray(list) &&
list?.map((v: any) => (
<Card
key={v.id}
radius="lg"
@@ -266,7 +273,13 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
Tanggal Ajuan
</Text>
</Group>
<Text size="md">{toDate(v.createdAt).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })}</Text>
<Text size="md">
{toDate(v.createdAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">

View File

@@ -16,7 +16,7 @@ import {
Table,
Text,
Textarea,
Title
Title,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
@@ -58,7 +58,12 @@ export default function DetailPengaduanPage() {
<Grid>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPengaduan data={data?.data?.pengaduan} onAction={() => { mutate(); }} />
<DetailDataPengaduan
data={data?.data?.pengaduan}
onAction={() => {
mutate();
}}
/>
<DetailDataHistori data={data?.data?.history} />
</Stack>
</Grid.Col>
@@ -70,7 +75,13 @@ export default function DetailPengaduanPage() {
);
}
function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: () => void }) {
function DetailDataPengaduan({
data,
onAction,
}: {
data: any | null;
onAction: () => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [openedPreview, setOpenedPreview] = useState(false);
@@ -84,7 +95,9 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
setHost(data?.user ?? null);
if (data?.permissions && Array.isArray(data.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("pengaduan"));
const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("pengaduan"),
);
setPermissions(onlySetting);
}
}
@@ -95,9 +108,16 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
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',
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: data.status == "diterima"
? "dikerjakan"
: "selesai",
keterangan: keterangan,
idUser: host?.id ?? ""
idUser: host?.id ?? "",
});
if (res?.status === 200) {
@@ -115,7 +135,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
@@ -124,11 +143,10 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
type: "error",
});
}
}
};
return (
<>
{/* MODAL KONFIRMASI */}
<Modal
opened={opened}
@@ -143,24 +161,46 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
</Text>
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
<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")}>
<Button
variant="filled"
color="red"
disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")}
>
Tolak
</Button>
</Group>
</>
) : (
<>
<Text>Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : data.status == 'diterima' ? 'mengerjakan' : 'menyelesaikan'} 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}>
Tidak
</Button>
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")}>
<Button
variant="filled"
color="green"
onClick={() => handleKonfirmasi("terima")}
>
Ya
</Button>
</Group>
@@ -169,7 +209,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
</Stack>
</Modal>
{/* MODAL GAMBAR */}
<ModalFile
open={openedPreview && !_.isEmpty(data?.image)}
@@ -259,18 +298,20 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
<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>
}
{data?.image != null && data?.image != "" ? (
<Anchor
href="#"
onClick={() => {
setOpenedPreview(true);
}}
>
Lihat Gambar
</Anchor>
) : (
<Text size="md" c="white">
-
</Text>
)}
</Flex>
</Stack>
</Grid.Col>
@@ -285,74 +326,76 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
{_.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>
)
}
{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>
) : <></>
}
{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>
@@ -391,25 +434,25 @@ function DetailDataHistori({ data }: { data: any }) {
</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>
))
}
{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>
</Stack>

View File

@@ -134,7 +134,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
take: "",
page: page.toString(),
},
})
}),
);
useShallowEffect(() => {
@@ -175,7 +175,6 @@ function ListPengaduan({ status }: { status: StatusKey }) {
const pageNow = data?.data?.page || 1;
const toDate = (d: any) => new Date(d);
return (
<Stack gap="xl">
<Grid>
@@ -197,8 +196,16 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Grid.Col>
<Grid.Col span={3}>
<Group justify="flex-end">
<Text size="sm" c="gray.5">{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination total={totalPage} value={page} onChange={setPage} withPages={false} />
<Text
size="sm"
c="gray.5"
>{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination
total={totalPage}
value={page}
onChange={setPage}
withPages={false}
/>
</Group>
</Grid.Col>
</Grid>
@@ -212,7 +219,8 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
Array.isArray(list) && list?.map((v: any) => (
Array.isArray(list) &&
list?.map((v: any) => (
<Card
key={v.id}
radius="lg"
@@ -272,7 +280,13 @@ function ListPengaduan({ status }: { status: StatusKey }) {
Tanggal Aduan
</Text>
</Group>
<Text size="md">{toDate(v.createdAt).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })}</Text>
<Text size="md">
{toDate(v.createdAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">

View File

@@ -5,19 +5,14 @@ import ProfileUser from "@/components/ProfileUser";
import UserRoleSetting from "@/components/UserRoleSetting";
import UserSetting from "@/components/UserSetting";
import apiFetch from "@/lib/apiFetch";
import {
Card,
Container,
Grid,
NavLink
} from "@mantine/core";
import { Card, Container, Grid, NavLink } from "@mantine/core";
import {
IconBuildingBank,
IconCategory2,
IconMailSpark,
IconUserCog,
IconUserScreen,
IconUsersGroup
IconUsersGroup,
} from "@tabler/icons-react";
import type { JsonValue } from "generated/prisma/runtime/library";
import { useEffect, useState } from "react";
@@ -33,7 +28,9 @@ export default function DetailSettingPage() {
async function fetchPermissions() {
const { data } = await apiFetch.api.user.find.get();
if (Array.isArray(data?.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("setting"));
const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("setting"),
);
setPermissions(onlySetting);
} else {
setPermissions([]);
@@ -42,7 +39,6 @@ export default function DetailSettingPage() {
fetchPermissions();
}, []);
const navItems = [
{
key: "setting.profile",
@@ -85,8 +81,7 @@ export default function DetailSettingPage() {
icon: <IconBuildingBank size={20} />,
label: "Desa",
description: "Manage desa information",
}
},
];
return (
@@ -104,17 +99,19 @@ export default function DetailSettingPage() {
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
{
navItems.filter((item) => permissions.includes(item.key)).map((item) => (
{navItems
.filter((item) => permissions.includes(item.key))
.map((item) => (
<NavLink
key={item.key}
href={'?type=' + item.path}
href={"?type=" + item.path}
label={item.label}
leftSection={item.icon}
active={type === item.path || (!type && item.path === 'profile')}
active={
type === item.path || (!type && item.path === "profile")
}
/>
))
}
))}
</Card>
</Grid.Col>
<Grid.Col span={9}>
@@ -130,17 +127,47 @@ export default function DetailSettingPage() {
}}
>
{type === "cat-pengaduan" ? (
<KategoriPengaduan permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pengaduan"))} />
<KategoriPengaduan
permissions={permissions.filter(
(p) =>
typeof p === "string" &&
p.startsWith("setting.kategori_pengaduan"),
)}
/>
) : type === "cat-pelayanan" ? (
<KategoriPelayananSurat permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pelayanan"))} />
<KategoriPelayananSurat
permissions={permissions.filter(
(p) =>
typeof p === "string" &&
p.startsWith("setting.kategori_pelayanan"),
)}
/>
) : type === "desa" ? (
<DesaSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.desa"))} />
<DesaSetting
permissions={permissions.filter(
(p) => typeof p === "string" && p.startsWith("setting.desa"),
)}
/>
) : type === "user" ? (
<UserSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.user."))} />
<UserSetting
permissions={permissions.filter(
(p) => typeof p === "string" && p.startsWith("setting.user."),
)}
/>
) : type === "role" ? (
<UserRoleSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.user_role"))} />
<UserRoleSetting
permissions={permissions.filter(
(p) =>
typeof p === "string" && p.startsWith("setting.user_role"),
)}
/>
) : (
<ProfileUser permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.profile"))} />
<ProfileUser
permissions={permissions.filter(
(p) =>
typeof p === "string" && p.startsWith("setting.profile"),
)}
/>
)}
</Card>
</Grid.Col>

View File

@@ -37,10 +37,13 @@ export default function DetailWargaPage() {
mutate();
}, []);
return (
<>
<LoadingOverlay visible={isLoading} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
<LoadingOverlay
visible={isLoading}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
/>
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={4}>
@@ -48,18 +51,29 @@ export default function DetailWargaPage() {
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataHistori data={data?.data?.pengaduan} kategori="pengaduan" />
<DetailDataHistori data={data?.data?.pelayanan} kategori="pelayanan" />
<DetailDataHistori
data={data?.data?.pengaduan}
kategori="pengaduan"
/>
<DetailDataHistori
data={data?.data?.pelayanan}
kategori="pelayanan"
/>
</Stack>
</Grid.Col>
</Grid>
</Container>
</>
);
}
function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan' | 'pelayanan' }) {
function DetailDataHistori({
data,
kategori,
}: {
data: any;
kategori: "pengaduan" | "pelayanan";
}) {
const navigate = useNavigate();
return (
@@ -85,43 +99,47 @@ function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan
<Table.Thead>
<Table.Tr>
<Table.Th>No {_.upperFirst(kategori)}</Table.Th>
<Table.Th>{kategori == "pengaduan" ? "Judul" : "Kategori"}</Table.Th>
<Table.Th>
{kategori == "pengaduan" ? "Judul" : "Kategori"}
</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
data?.length > 0 ? (
data?.map((item: any, index: number) => (
<Table.Tr key={index}>
<Table.Td>{item.noPengaduan}</Table.Td>
<Table.Td>{kategori == "pengaduan" ? item.title : item.category}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>
<Button
variant="outline"
onClick={() => {
kategori == "pengaduan" ?
navigate(
{data?.length > 0 ? (
data?.map((item: any, index: number) => (
<Table.Tr key={index}>
<Table.Td>{item.noPengaduan}</Table.Td>
<Table.Td>
{kategori == "pengaduan" ? item.title : item.category}
</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>
<Button
variant="outline"
onClick={() => {
kategori == "pengaduan"
? navigate(
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
) :
navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
)
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
))
) : (
<Table.Tr>
<Table.Td colSpan={4} align="center">Tidak ada data</Table.Td>
: navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
)
}
))
) : (
<Table.Tr>
<Table.Td colSpan={4} align="center">
Tidak ada data
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Stack>

View File

@@ -39,7 +39,6 @@ export default function ListWargaPage() {
const pageSize = data?.data?.pageSize || 10;
const pageNow = data?.data?.page || 1;
useShallowEffect(() => {
setPages(1);
mutate();
@@ -49,7 +48,6 @@ export default function ListWargaPage() {
mutate();
}, [pages]);
return (
<Container size="xl" py="xl" w={"100%"}>
<Card
@@ -82,7 +80,12 @@ export default function ListWargaPage() {
/>
<Group>
<Text size="sm">{`${pageSize * (pages - 1) + 1} ${Math.min(total, pageSize * pages)} of ${total}`}</Text>
<Pagination total={totalPage} value={pages} onChange={setPages} withPages={false} />
<Pagination
total={totalPage}
value={pages}
onChange={setPages}
withPages={false}
/>
</Group>
</Flex>
<Divider my={0} />
@@ -95,32 +98,33 @@ export default function ListWargaPage() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
Array.isArray(list) && list?.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
{Array.isArray(list) && list?.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={3} align="center">
Tidak ada data
</Table.Td>
</Table.Tr>
) : (
Array.isArray(list) &&
list?.map((item, i) => (
<Table.Tr key={i}>
<Table.Td>{item.name}</Table.Td>
<Table.Td w={250}>{item.phone}</Table.Td>
<Table.Td w={150}>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
) : (
Array.isArray(list) && list?.map((item, i) => (
<Table.Tr key={i}>
<Table.Td>{item.name}</Table.Td>
<Table.Td w={250}>{item.phone}</Table.Td>
<Table.Td w={150}>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
))
)
}
))
)}
</Table.Tbody>
</Table>
</Stack>