Deskripsi: - pengaplikasian api - modal konfirmasi update pelayanan surat - modal konfirmasi create pelayanan surat NO Issues
621 lines
18 KiB
TypeScript
621 lines
18 KiB
TypeScript
import FullScreenLoading from "@/components/FullScreenLoading";
|
||
import ModalFile from "@/components/ModalFile";
|
||
import notification from "@/components/notificationGlobal";
|
||
import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
|
||
import apiFetch from "@/lib/apiFetch";
|
||
import {
|
||
ActionIcon,
|
||
Alert,
|
||
Anchor,
|
||
Badge,
|
||
Box,
|
||
Button,
|
||
Card,
|
||
Container,
|
||
Divider,
|
||
FileInput,
|
||
Flex,
|
||
Grid,
|
||
Group,
|
||
Modal,
|
||
Stack,
|
||
Text,
|
||
TextInput,
|
||
Tooltip
|
||
} from "@mantine/core";
|
||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||
import {
|
||
IconBuildingCommunity,
|
||
IconInfoCircle,
|
||
IconUpload
|
||
} from "@tabler/icons-react";
|
||
import _ from "lodash";
|
||
import React, { useState } from "react";
|
||
import { useLocation, useNavigate } from "react-router-dom";
|
||
|
||
type DataItem = {
|
||
key: string;
|
||
value: string;
|
||
};
|
||
|
||
type UpdateDataItem = {
|
||
id: string;
|
||
key: string;
|
||
value: any;
|
||
};
|
||
|
||
type FormSurat = {
|
||
kategoriId: string;
|
||
nama: string;
|
||
phone: string;
|
||
dataPelengkap: DataItem[];
|
||
syaratDokumen: DataItem[];
|
||
};
|
||
|
||
type FormUpdateSurat = {
|
||
dataPelengkap: UpdateDataItem[];
|
||
syaratDokumen: UpdateDataItem[];
|
||
};
|
||
|
||
type DataPengajuan = {
|
||
id: string;
|
||
noPengajuan: string;
|
||
category: string;
|
||
status: "antrian" | "diproses" | "selesai" | "ditolak";
|
||
createdAt: Date;
|
||
updatedAt: Date;
|
||
idSurat: string | undefined;
|
||
}
|
||
|
||
export default function UpdateDataSurat() {
|
||
const navigate = useNavigate();
|
||
const { search } = useLocation();
|
||
const query = new URLSearchParams(search);
|
||
const noPengajuan = query.get("pengajuan");
|
||
|
||
|
||
return (
|
||
<Container size="md" w={"100%"}>
|
||
<Box>
|
||
<Stack gap="lg">
|
||
<Group justify="space-between" align="center" mt={"lg"}>
|
||
<Group align="center">
|
||
<IconBuildingCommunity size={28} />
|
||
<div>
|
||
<Text fw={800} size="xl">
|
||
Update Data Pengajuan Surat Administrasi
|
||
</Text>
|
||
<Text size="sm" c="dimmed">
|
||
Formulir ini digunakan untuk memperbarui data pengajuan surat administrasi yang telah diajukan sebelumnya.
|
||
</Text>
|
||
</div>
|
||
</Group>
|
||
</Group>
|
||
<Stack gap="lg" mb="lg">
|
||
{
|
||
!noPengajuan ? (
|
||
<SearchData />
|
||
)
|
||
:
|
||
<DataUpdate noPengajuan={noPengajuan} />
|
||
}
|
||
|
||
</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,
|
||
info,
|
||
}: {
|
||
title?: string;
|
||
icon?: React.ReactNode;
|
||
children: React.ReactNode;
|
||
description?: string;
|
||
info?: string;
|
||
}) {
|
||
return (
|
||
<Card radius="md" shadow="sm" withBorder>
|
||
<Box mb="xs">
|
||
<Group justify="apart" align="center">
|
||
<Group align="center" gap="xs">
|
||
{icon}
|
||
{title && <Text fw={700}>{title}</Text>}
|
||
</Group>
|
||
{description && <Badge variant="light">{description}</Badge>}
|
||
</Group>
|
||
{info && <Text size="sm" c="dimmed">{info}</Text>}
|
||
</Box>
|
||
{
|
||
title && <Divider mb="sm" />
|
||
}
|
||
<Stack gap="sm">{children}</Stack>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
function FileInputWrapper({
|
||
label,
|
||
placeholder,
|
||
accept,
|
||
onChange,
|
||
preview,
|
||
name,
|
||
description,
|
||
linkView,
|
||
disabled
|
||
}: {
|
||
label: string;
|
||
placeholder?: string;
|
||
accept?: string;
|
||
linkView?: string;
|
||
onChange: (file: File | null) => void;
|
||
preview?: string | null;
|
||
name: string;
|
||
description?: string;
|
||
disabled?: boolean;
|
||
}) {
|
||
const [viewImg, setViewImg] = useState("");
|
||
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
|
||
|
||
|
||
useShallowEffect(() => {
|
||
if (viewImg) {
|
||
setOpenedPreviewFile(true);
|
||
}
|
||
}, [viewImg]);
|
||
|
||
return (
|
||
<>
|
||
<ModalFile
|
||
open={openedPreviewFile && !_.isEmpty(viewImg)}
|
||
onClose={() => {
|
||
setOpenedPreviewFile(false);
|
||
}}
|
||
folder="syarat-dokumen"
|
||
fileName={viewImg}
|
||
/>
|
||
|
||
<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>
|
||
)}
|
||
{
|
||
linkView && (
|
||
<Anchor onClick={() => setViewImg(linkView)} size="sm">
|
||
Lihat dokumen sebelumnya
|
||
</Anchor>
|
||
)
|
||
}
|
||
</Flex>
|
||
|
||
<FileInput
|
||
accept={accept}
|
||
placeholder={placeholder}
|
||
onChange={(f) => onChange(f)}
|
||
leftSection={<IconUpload />}
|
||
aria-label={label}
|
||
name={name}
|
||
disabled={disabled}
|
||
/>
|
||
|
||
{preview ? (
|
||
<div>
|
||
<Text size="xs" color="dimmed">
|
||
Preview:
|
||
</Text>
|
||
<div style={{ marginTop: 6 }}>
|
||
<img
|
||
src={preview}
|
||
alt={`${label} preview`}
|
||
style={{ maxWidth: "200px", borderRadius: 4 }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</Stack>
|
||
</>
|
||
|
||
);
|
||
}
|
||
|
||
function SearchData() {
|
||
const [submitLoading, setSubmitLoading] = useState(false);
|
||
const [searchPengajuan, setSearchPengajuan] = useState("");
|
||
const [searchPengajuanPhone, setSearchPengajuanPhone] = useState("");
|
||
const navigate = useNavigate();
|
||
|
||
async function handleSearch() {
|
||
try {
|
||
setSubmitLoading(true);
|
||
if (searchPengajuan == "" || searchPengajuanPhone == "") {
|
||
notification({
|
||
title: "Peringatan",
|
||
message: "Silakan isi nomor pengajuan atau nomor telephone",
|
||
type: "warning"
|
||
});
|
||
return;
|
||
}
|
||
|
||
const response = await apiFetch.api.pelayanan["get-no-pengajuan"].post({
|
||
phone: searchPengajuanPhone,
|
||
noPengajuan: searchPengajuan
|
||
});
|
||
|
||
if (response.status === 200) {
|
||
if (response.data?.success) {
|
||
navigate(`/darmasaba/update-data-surat?pengajuan=${response.data.nomer}`);
|
||
} else {
|
||
notification({
|
||
title: "Peringatan",
|
||
message: response.data?.message || "Data pengajuan tidak valid",
|
||
type: "warning"
|
||
});
|
||
}
|
||
} else {
|
||
notification({
|
||
title: "Error",
|
||
message: "Pengajuan tidak ditemukan atau gagal memuat data",
|
||
type: "error"
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error("Error searching:", error);
|
||
notification({
|
||
title: "Error",
|
||
message: "Gagal mencari data pengajuan",
|
||
type: "error"
|
||
});
|
||
} finally {
|
||
setSubmitLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<FormSection
|
||
title="Cari Pengajuan Surat"
|
||
info="Masukkan nomor pengajuan dan nomor telepon yang digunakan saat pengajuan surat."
|
||
>
|
||
<Grid>
|
||
<Grid.Col span={6}>
|
||
<TextInput
|
||
label={<FieldLabel label="Nomor Pengajuan" hint="Nomor pengajuan surat" />}
|
||
placeholder="PS-2025-000123"
|
||
onChange={(e) => { setSearchPengajuan(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"
|
||
type="number"
|
||
onChange={(e) => { setSearchPengajuanPhone(e.target.value) }}
|
||
/>
|
||
</Grid.Col>
|
||
|
||
<Grid.Col span={12}>
|
||
<Button fullWidth variant="light" color="blue" onClick={() => { handleSearch() }} loading={submitLoading}>
|
||
Cari Pengajuan
|
||
</Button>
|
||
</Grid.Col>
|
||
</Grid>
|
||
</FormSection>
|
||
)
|
||
}
|
||
|
||
|
||
function DataUpdate({ noPengajuan }: { noPengajuan: string }) {
|
||
const [opened, { open, close }] = useDisclosure(false)
|
||
const navigate = useNavigate()
|
||
const [sukses, setSukses] = useState(false)
|
||
const [submitLoading, setSubmitLoading] = useState(false)
|
||
const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([])
|
||
const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([])
|
||
const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({})
|
||
const [status, setStatus] = useState("")
|
||
const [formSurat, setFormSurat] = useState<FormUpdateSurat>({
|
||
dataPelengkap: [],
|
||
syaratDokumen: [],
|
||
});
|
||
|
||
async function fetchData() {
|
||
try {
|
||
const res = await apiFetch.api.pelayanan["detail-data"].post({ nomerPengajuan: noPengajuan });
|
||
if (res.data && res.data.success === true) {
|
||
setDataPelengkap(res.data.dataPelengkap || []);
|
||
setDataSyaratDokumen(res.data.syaratDokumen || []);
|
||
setDataPengajuan(res.data.pengajuan || {});
|
||
|
||
setStatus(res.data.pengajuan && 'status' in res.data.pengajuan ? res.data.pengajuan.status : "");
|
||
} else {
|
||
notification({
|
||
title: "Error",
|
||
message: res.data?.message || "Gagal memuat data",
|
||
type: "error",
|
||
});
|
||
setDataPelengkap([]);
|
||
setDataSyaratDokumen([]);
|
||
setDataPengajuan({});
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching data:', error);
|
||
}
|
||
}
|
||
|
||
useShallowEffect(() => {
|
||
fetchData();
|
||
}, []);
|
||
|
||
function upsertById<T extends { id: string }>(
|
||
array: T[],
|
||
item: T
|
||
): T[] {
|
||
const index = array.findIndex((v) => v.id === item.id)
|
||
|
||
// ➕ insert
|
||
if (index === -1) {
|
||
return [...array, item]
|
||
}
|
||
|
||
// ✏️ update
|
||
return array.map((v, i) => (i === index ? { ...v, ...item } : v))
|
||
}
|
||
|
||
|
||
function validationForm({
|
||
kategori,
|
||
value,
|
||
}: {
|
||
kategori: "dataPelengkap" | "syaratDokumen";
|
||
value: UpdateDataItem;
|
||
}) {
|
||
setFormSurat((prev) => ({
|
||
...prev,
|
||
[kategori]: upsertById(prev[kategori], {
|
||
id: value.id,
|
||
key: value.key,
|
||
value: value.value
|
||
})
|
||
}));
|
||
}
|
||
|
||
function updateArrayByKey(
|
||
list: UpdateDataItem[],
|
||
id: string,
|
||
value: any,
|
||
): UpdateDataItem[] {
|
||
return list.map((item) =>
|
||
item.id === id ? { ...item, value } : item,
|
||
);
|
||
}
|
||
|
||
function onChecking() {
|
||
if (formSurat.dataPelengkap.length == 0 && formSurat.syaratDokumen.length == 0)
|
||
return notification({
|
||
title: "Peringatan",
|
||
message: "Tidak ada data yang diupdate",
|
||
type: "warning",
|
||
});
|
||
const isFormKosong = Object.values(formSurat).some((value: UpdateDataItem[] | string) => {
|
||
if (Array.isArray(value)) {
|
||
return (
|
||
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",
|
||
});
|
||
} else {
|
||
open()
|
||
}
|
||
}
|
||
|
||
async function onSubmit() {
|
||
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.id,
|
||
updImg.data?.filename || "",
|
||
);
|
||
}
|
||
}
|
||
|
||
// 3️⃣ SET STATE SEKALI (optional, untuk UI)
|
||
setFormSurat(finalFormSurat);
|
||
|
||
// 4️⃣ SUBMIT KE API
|
||
const res = await apiFetch.api.pelayanan.update.post({
|
||
id: dataPengajuan && ('id' in dataPengajuan) ? dataPengajuan.id : "",
|
||
dataPelengkap: finalFormSurat.dataPelengkap,
|
||
syaratDokumen: finalFormSurat.syaratDokumen,
|
||
});
|
||
|
||
if (res.status === 200) {
|
||
setSukses(true);
|
||
} 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);
|
||
}
|
||
}
|
||
|
||
|
||
return (
|
||
<>
|
||
<FullScreenLoading visible={submitLoading} />
|
||
<Modal
|
||
opened={opened}
|
||
onClose={close}
|
||
title={"Konfirmasi"}
|
||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||
>
|
||
<Stack gap="sm">
|
||
<Text>
|
||
Apakah anda yakin ingin mengupdate pengajuan surat ini?
|
||
</Text>
|
||
<Group justify="center" grow>
|
||
<Button variant="light" onClick={close}>
|
||
Tidak
|
||
</Button>
|
||
<Button
|
||
variant="filled"
|
||
color="green"
|
||
onClick={() => {
|
||
onSubmit();
|
||
close();
|
||
}}
|
||
>
|
||
Ya
|
||
</Button>
|
||
</Group>
|
||
</Stack>
|
||
</Modal>
|
||
{
|
||
sukses ?
|
||
<SuccessPengajuan
|
||
noPengajuan={noPengajuan}
|
||
onClose={() => {
|
||
navigate("/darmasaba/update-data-surat");
|
||
}}
|
||
category="update"
|
||
/>
|
||
:
|
||
<>
|
||
{
|
||
(status != "ditolak" && status != "antrian")
|
||
&& <Alert variant="light" color="yellow" radius="lg" title={`Data pengajuan surat ini tidak dapat diupdate karena berstatus ${status}.`} icon={<span style={{ fontSize: '1.2rem' }}>⚠</span>} />
|
||
}
|
||
<FormSection
|
||
title="Data Pelengkap"
|
||
description="Data pelengkap yang diperlukan"
|
||
>
|
||
<Grid>
|
||
{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({
|
||
kategori: "dataPelengkap",
|
||
value: { id: item.id, key: item.key, value: e.target.value },
|
||
})
|
||
}
|
||
value={formSurat.dataPelengkap.find((n) => n.id === item.id)?.value ?? dataPelengkap.find((n: any) => n.key == item.key,)?.value}
|
||
disabled={status != "ditolak" && status != "antrian"}
|
||
/>
|
||
</Grid.Col>
|
||
))}
|
||
</Grid>
|
||
</FormSection>
|
||
|
||
<FormSection
|
||
title="Syarat Dokumen"
|
||
description="Syarat dokumen yang diperlukan"
|
||
>
|
||
<Grid>
|
||
{dataSyaratDokumen.map((item: any, index: number) => (
|
||
<Grid.Col span={6} key={index}>
|
||
<FileInputWrapper
|
||
label={item.desc}
|
||
placeholder={"Upload file terbaru untuk mengupdate"}
|
||
accept="image/*,application/pdf"
|
||
linkView={item.value}
|
||
onChange={(file) =>
|
||
validationForm({
|
||
kategori: "syaratDokumen",
|
||
value: { id: item.id, key: item.key, value: file },
|
||
})
|
||
}
|
||
name={item.name}
|
||
disabled={status != "ditolak" && status != "antrian"}
|
||
/>
|
||
</Grid.Col>
|
||
))}
|
||
</Grid>
|
||
</FormSection>
|
||
|
||
<Group justify="right" mt="md">
|
||
<Button onClick={() => { onChecking() }}>Kirim</Button>
|
||
</Group>
|
||
</>
|
||
}
|
||
</>
|
||
)
|
||
}
|