Deskripsi - form pencarian - detail data pengajuan - api - belom selesai submit NO Issues
514 lines
14 KiB
TypeScript
514 lines
14 KiB
TypeScript
import FullScreenLoading from "@/components/FullScreenLoading";
|
||
import notification from "@/components/notificationGlobal";
|
||
import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
|
||
import apiFetch from "@/lib/apiFetch";
|
||
import { fromSlug, toSlug } from "@/server/lib/slug_converter";
|
||
import {
|
||
ActionIcon,
|
||
Badge,
|
||
Box,
|
||
Button,
|
||
Card,
|
||
Container,
|
||
Divider,
|
||
FileInput,
|
||
Flex,
|
||
Grid,
|
||
Group,
|
||
Select,
|
||
Stack,
|
||
Text,
|
||
TextInput,
|
||
Tooltip,
|
||
} from "@mantine/core";
|
||
import { useShallowEffect } from "@mantine/hooks";
|
||
import {
|
||
IconBuildingCommunity,
|
||
IconInfoCircle,
|
||
IconUpload,
|
||
IconUser,
|
||
} from "@tabler/icons-react";
|
||
import React, { useState } from "react";
|
||
import { useLocation, useNavigate } from "react-router-dom";
|
||
import useSWR from "swr";
|
||
|
||
type DataItem = {
|
||
key: string;
|
||
value: string;
|
||
};
|
||
|
||
type FormSurat = {
|
||
kategoriId: string;
|
||
nama: string;
|
||
phone: string;
|
||
dataPelengkap: DataItem[];
|
||
syaratDokumen: DataItem[];
|
||
};
|
||
|
||
export default function FormSurat() {
|
||
const [noPengajuan, setNoPengajuan] = useState("");
|
||
const [submitLoading, setSubmitLoading] = useState(false);
|
||
const navigate = useNavigate();
|
||
const { search } = useLocation();
|
||
const query = new URLSearchParams(search);
|
||
const jenisSurat = query.get("jenis");
|
||
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
|
||
apiFetch.api.pelayanan.category.get(),
|
||
);
|
||
const [jenisSuratFix, setJenisSuratFix] = useState({ name: "", id: "" });
|
||
const [dataSurat, setDataSurat] = useState<any>({});
|
||
const [formSurat, setFormSurat] = useState<FormSurat>({
|
||
nama: "",
|
||
phone: "",
|
||
kategoriId: "",
|
||
dataPelengkap: [],
|
||
syaratDokumen: [],
|
||
});
|
||
|
||
const listCategory = data?.data || [];
|
||
|
||
function onResetAll() {
|
||
setNoPengajuan("");
|
||
setSubmitLoading(false);
|
||
setFormSurat({
|
||
nama: "",
|
||
phone: "",
|
||
kategoriId: "",
|
||
dataPelengkap: [],
|
||
syaratDokumen: [],
|
||
});
|
||
}
|
||
|
||
function onGetJenisSurat() {
|
||
try {
|
||
if (!jenisSurat || jenisSurat == "null") {
|
||
setJenisSuratFix({ name: "", id: "" });
|
||
} else {
|
||
const namaJenis = fromSlug(jenisSurat);
|
||
const data = listCategory.find((item: any) => item.name == namaJenis);
|
||
if (!data) return;
|
||
setJenisSuratFix(data);
|
||
}
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function getDetailJenisSurat() {
|
||
try {
|
||
const get: any = await apiFetch.api.pelayanan.category.detail.get({
|
||
query: {
|
||
id: jenisSuratFix.id,
|
||
},
|
||
});
|
||
|
||
setDataSurat(get.data);
|
||
setFormSurat({
|
||
kategoriId: jenisSuratFix.id,
|
||
nama: "",
|
||
phone: "",
|
||
dataPelengkap: (get.data?.dataPelengkap || []).map(
|
||
(item: { key: string }) => ({
|
||
key: item.key,
|
||
value: "",
|
||
}),
|
||
),
|
||
syaratDokumen: (get.data?.syaratDokumen || []).map(
|
||
(item: { key: string }) => ({
|
||
key: item.key,
|
||
value: "",
|
||
}),
|
||
),
|
||
});
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
useShallowEffect(() => {
|
||
mutate();
|
||
}, []);
|
||
|
||
useShallowEffect(() => {
|
||
if (listCategory.length > 0) {
|
||
onGetJenisSurat();
|
||
}
|
||
}, [jenisSurat, listCategory]);
|
||
|
||
useShallowEffect(() => {
|
||
if (jenisSuratFix.id != "") {
|
||
getDetailJenisSurat();
|
||
}
|
||
}, [jenisSuratFix.id]);
|
||
|
||
async function onSubmit() {
|
||
const isFormKosong = Object.values(formSurat).some((value) => {
|
||
if (Array.isArray(value)) {
|
||
return (
|
||
value.length === 0 ||
|
||
value.some(
|
||
(item) =>
|
||
typeof item.value === "string" && item.value.trim() === "",
|
||
)
|
||
);
|
||
}
|
||
|
||
if (typeof value === "string") {
|
||
return value.trim() === "";
|
||
}
|
||
|
||
return false;
|
||
});
|
||
|
||
if (isFormKosong) {
|
||
return notification({
|
||
title: "Gagal",
|
||
message: "Silahkan lengkapi form surat",
|
||
type: "error",
|
||
});
|
||
}
|
||
|
||
try {
|
||
setSubmitLoading(true);
|
||
// 🔥 CLONE state SEKALI
|
||
let finalFormSurat = structuredClone(formSurat);
|
||
|
||
// 2️⃣ Upload satu per satu
|
||
for (const itemUpload of finalFormSurat.syaratDokumen) {
|
||
const updImg = await apiFetch.api.pengaduan.upload.post({
|
||
file: itemUpload.value,
|
||
folder: "syarat-dokumen",
|
||
});
|
||
|
||
if (updImg.status === 200) {
|
||
// 🔥 UPDATE OBJECT LOKAL (BUKAN STATE)
|
||
finalFormSurat.syaratDokumen = updateArrayByKey(
|
||
finalFormSurat.syaratDokumen,
|
||
itemUpload.key,
|
||
updImg.data?.filename || "",
|
||
);
|
||
}
|
||
}
|
||
|
||
// 3️⃣ SET STATE SEKALI (optional, untuk UI)
|
||
setFormSurat(finalFormSurat);
|
||
|
||
// 4️⃣ SUBMIT KE API
|
||
const res = await apiFetch.api.pelayanan.create.post(finalFormSurat);
|
||
|
||
if (res.status === 200) {
|
||
notification({
|
||
title: "Berhasil",
|
||
message: res.data?.message || "Pengajuan surat berhasil dibuat",
|
||
type: "success",
|
||
});
|
||
} else {
|
||
notification({
|
||
title: "Gagal",
|
||
message:
|
||
"Pengajuan surat gagal dibuat, silahkan coba beberapa saat lagi",
|
||
type: "error",
|
||
});
|
||
}
|
||
} catch (error) {
|
||
notification({
|
||
title: "Gagal",
|
||
message: "Server Error",
|
||
type: "error",
|
||
});
|
||
} finally {
|
||
setSubmitLoading(false);
|
||
}
|
||
}
|
||
|
||
function updateArrayByKey(
|
||
list: DataItem[],
|
||
targetKey: string,
|
||
value: string,
|
||
): DataItem[] {
|
||
return list.map((item) =>
|
||
item.key === targetKey ? { ...item, value } : item,
|
||
);
|
||
}
|
||
|
||
function validationForm({
|
||
key,
|
||
value,
|
||
}: {
|
||
key: "nama" | "phone" | "dataPelengkap" | "syaratDokumen";
|
||
value: any;
|
||
}) {
|
||
if (key == "dataPelengkap" || key == "syaratDokumen") {
|
||
setFormSurat((prev) => ({
|
||
...prev,
|
||
[key]: updateArrayByKey(prev[key], value.key, value.value),
|
||
}));
|
||
} else {
|
||
setFormSurat({
|
||
...formSurat,
|
||
[key]: value,
|
||
});
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Container size="md" w={"100%"}>
|
||
<FullScreenLoading visible={submitLoading} />
|
||
{noPengajuan != "" && (
|
||
<SuccessPengajuan
|
||
noPengajuan={noPengajuan}
|
||
onClose={() => {
|
||
onResetAll();
|
||
navigate("/darmasaba/surat");
|
||
}}
|
||
/>
|
||
)}
|
||
<Box>
|
||
<Stack gap="lg">
|
||
<Group justify="space-between" align="center">
|
||
<Group align="center">
|
||
<IconBuildingCommunity size={28} />
|
||
<div>
|
||
<Text fw={800} size="xl">
|
||
Layanan Pengajuan Surat Administrasi
|
||
</Text>
|
||
<Text size="sm" c="dimmed">
|
||
Formulir resmi untuk mengajukan berbagai jenis surat
|
||
administrasi desa/kelurahan secara online.
|
||
</Text>
|
||
</div>
|
||
</Group>
|
||
<Group>
|
||
<Badge radius="sm">Form Length: 3 Sections</Badge>
|
||
</Group>
|
||
</Group>
|
||
<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" hint="Nama pemohon" />}
|
||
placeholder="Budi Setiawan"
|
||
value={formSurat.nama}
|
||
onChange={(e) =>
|
||
validationForm({ key: "nama", value: e.target.value })
|
||
}
|
||
/>
|
||
</Grid.Col>
|
||
|
||
<Grid.Col span={6}>
|
||
<TextInput
|
||
label={
|
||
<FieldLabel
|
||
label="Nomor Telephone"
|
||
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
|
||
/>
|
||
}
|
||
placeholder="08123456789"
|
||
value={formSurat.phone}
|
||
onChange={(e) =>
|
||
validationForm({ key: "phone", value: e.target.value })
|
||
}
|
||
/>
|
||
</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.name,
|
||
label: item.name,
|
||
}))}
|
||
value={jenisSuratFix.name}
|
||
onChange={(value) => {
|
||
const slug = toSlug(String(value));
|
||
navigate("/darmasaba/surat?jenis=" + slug);
|
||
}}
|
||
/>
|
||
</Grid.Col>
|
||
</Grid>
|
||
</FormSection>
|
||
|
||
{jenisSuratFix.id != "" && dataSurat && dataSurat.dataPelengkap && (
|
||
<>
|
||
<FormSection
|
||
title="Data Pelengkap"
|
||
description="Data pelengkap yang diperlukan"
|
||
>
|
||
<Grid>
|
||
{dataSurat.dataPelengkap.map((item: any, index: number) => (
|
||
<Grid.Col span={6} key={index}>
|
||
<TextInput
|
||
label={
|
||
<FieldLabel label={item.name} hint={item.desc} />
|
||
}
|
||
placeholder={item.name}
|
||
onChange={(e) =>
|
||
validationForm({
|
||
key: "dataPelengkap",
|
||
value: { key: item.key, value: e.target.value },
|
||
})
|
||
}
|
||
value={
|
||
formSurat.dataPelengkap.find(
|
||
(n: any) => n.key == item.key,
|
||
)?.value
|
||
}
|
||
/>
|
||
</Grid.Col>
|
||
))}
|
||
</Grid>
|
||
</FormSection>
|
||
|
||
<FormSection
|
||
title="Syarat Dokumen"
|
||
description="Syarat dokumen yang diperlukan"
|
||
>
|
||
<Grid>
|
||
{dataSurat.syaratDokumen.map((item: any, index: number) => (
|
||
<Grid.Col span={6} key={index}>
|
||
<FileInputWrapper
|
||
label={item.desc}
|
||
placeholder={"Upload file "}
|
||
accept="image/*,application/pdf"
|
||
onChange={(file) =>
|
||
validationForm({
|
||
key: "syaratDokumen",
|
||
value: { key: item.key, value: file },
|
||
})
|
||
}
|
||
name={item.name}
|
||
/>
|
||
</Grid.Col>
|
||
))}
|
||
</Grid>
|
||
</FormSection>
|
||
|
||
{/* Actions */}
|
||
<Group justify="right" mt="md">
|
||
<Button variant="default" onClick={() => {}}>
|
||
Reset
|
||
</Button>
|
||
<Button onClick={onSubmit}>Kirim</Button>
|
||
</Group>
|
||
</>
|
||
)}
|
||
</Stack>
|
||
</Stack>
|
||
</Box>
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
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>
|
||
);
|
||
}
|
||
|
||
function FileInputWrapper({
|
||
label,
|
||
placeholder,
|
||
accept,
|
||
onChange,
|
||
preview,
|
||
name,
|
||
description,
|
||
}: {
|
||
label: string;
|
||
placeholder?: string;
|
||
accept?: string;
|
||
onChange: (file: File | null) => void;
|
||
preview?: string | null;
|
||
name: string;
|
||
description?: string;
|
||
}) {
|
||
return (
|
||
<Stack gap="xs">
|
||
<Flex direction={"column"}>
|
||
<Group justify="apart" align="center">
|
||
<Text fw={500}>{label}</Text>
|
||
</Group>
|
||
{description && (
|
||
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
|
||
{description}
|
||
</Text>
|
||
)}
|
||
</Flex>
|
||
|
||
<FileInput
|
||
accept={accept}
|
||
placeholder={placeholder}
|
||
onChange={(f) => onChange(f)}
|
||
leftSection={<IconUpload />}
|
||
aria-label={label}
|
||
name={name}
|
||
/>
|
||
|
||
{preview ? (
|
||
<div>
|
||
<Text size="xs" color="dimmed">
|
||
Preview:
|
||
</Text>
|
||
{/* If preview is an image it will show; pdf preview might not render as image */}
|
||
{/* Use <object> or <img> depending on file type — keep simple here */}
|
||
<div style={{ marginTop: 6 }}>
|
||
<img
|
||
src={preview}
|
||
alt={`${label} preview`}
|
||
style={{ maxWidth: "200px", borderRadius: 4 }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</Stack>
|
||
);
|
||
}
|