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, Modal, Select, Stack, Text, TextInput, Tooltip, } from "@mantine/core"; import { DateInput } from "@mantine/dates"; import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconBuildingCommunity, IconCategory, IconFiles, IconInfoCircle, IconNotes, IconPhone, IconUpload } from "@tabler/icons-react"; import dayjs from "dayjs"; import "dayjs/locale/id"; import React, { useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import useSWR from "swr"; type DataItem = { key: string; value: string; required: boolean; }; type FormSurat = { kategoriId: string; nama: string; phone: string; dataPelengkap: DataItem[]; syaratDokumen: DataItem[]; }; type ErrorState = Record; export default function FormSurat() { const [errors, setErrors] = useState({}); const [opened, { open, close }] = useDisclosure(false); 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({}); const [formSurat, setFormSurat] = useState({ 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.toUpperCase() == namaJenis.toUpperCase()); 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, required: boolean }) => ({ key: item.key, value: "", required: item.required }), ), syaratDokumen: (get.data?.syaratDokumen || []).map( (item: { key: string, required: boolean }) => ({ key: item.key, value: "", required: item.required }), ), }); } catch (error) { console.error(error); } } useShallowEffect(() => { mutate(); }, []); useShallowEffect(() => { if (listCategory.length > 0) { onGetJenisSurat(); } }, [jenisSurat, listCategory]); useShallowEffect(() => { if (jenisSuratFix.id != "") { getDetailJenisSurat(); } }, [jenisSuratFix.id]); function onChecking() { const hasError = Object.values(errors).some((v) => v); if (hasError) { return notification({ title: "Gagal", message: "Masih ada form yang belum valid", type: "error", }); } const isFormKosong = Object.values(formSurat).some((value) => { if (Array.isArray(value)) { return value.some( (item) => (typeof item.value === "string" && item.value.trim() === "" && item.required) || (typeof item.value === "object" && item.value === null && item.required), ); } return typeof value === "string" && value.trim() === ""; }); if (isFormKosong) { return notification({ title: "Gagal", message: "Silahkan lengkapi form surat", type: "error", }); } 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.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) { setNoPengajuan(res.data?.noPengajuan || ""); } 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 validateField(key: string, value: any) { const stringValue = String(value ?? "").trim(); // wajib diisi if (!stringValue) { return "Field wajib diisi"; } // 🔥 semua key yang mengandung "nik" if (key.toLowerCase().includes("nik")) { if (!/^\d+$/.test(stringValue)) { return "NIK harus berupa angka"; } if (stringValue.length !== 16) { return "NIK harus 16 digit"; } } return null; } function validationForm({ key, value, }: { key: "nama" | "phone" | "dataPelengkap" | "syaratDokumen"; value: any; }) { if (key === "dataPelengkap" || key === "syaratDokumen") { if (value.required == true) { const errorMsg = validateField(value.key, value.value); setErrors((prev) => ({ ...prev, [value.key]: errorMsg, })); } setFormSurat((prev) => ({ ...prev, [key]: updateArrayByKey(prev[key], value.key, value.value), })); } else { const keyFix = key == "nama" ? "nama_kontak" : key; const errorMsg = validateField(keyFix, value); setErrors((prev) => ({ ...prev, [keyFix]: errorMsg, })); setFormSurat({ ...formSurat, [key]: value, }); } } return ( Apakah anda yakin ingin mengirim pengajuan surat ini? {noPengajuan != "" ? ( { onResetAll(); navigate("/darmasaba/surat"); }} category="create" /> ) : (
Layanan Pengajuan Surat Administrasi Formulir resmi untuk mengajukan berbagai jenis surat administrasi desa/kelurahan secara online.
Form Length: 4 Sections
} > } data={item.options ?? []} placeholder={item.name} onChange={(e) => { validationForm({ key: "dataPelengkap", value: { key: item.key, value: e, required: item.required }, }); }} value={ formSurat.dataPelengkap.find( (n: any) => n.key == item.key, )?.value } /> ) : item.type == "date" ? ( } placeholder={item.name} onChange={(e) => { const formatted = e ? dayjs(e) .locale("id") .format("DD MMMM YYYY") : ""; validationForm({ key: "dataPelengkap", value: { key: item.key, value: formatted, required: item.required, }, }); }} /> ) : ( } placeholder={item.name} onChange={(e) => validationForm({ key: "dataPelengkap", value: { key: item.key, value: e.target.value, required: item.required, }, }) } value={ formSurat.dataPelengkap.find( (n: any) => n.key == item.key, )?.value } rightSection={ item.satuan != null && {item.satuan} } /> )} ), )} } > {dataSurat.syaratDokumen.map( (item: any, index: number) => ( validationForm({ key: "syaratDokumen", value: { key: item.key, value: file }, }) } name={item.name} required={item.required} /> ), )} {/* Actions */} {/* */} )}
)}
); } function FieldLabel({ label, hint, required = false, }: { label: string; hint?: string; required?: boolean; }) { return ( {label} {required && ( * )} {hint && ( )} ); } function FormSection({ title, icon, children, description, }: { title: string; icon?: React.ReactNode; children: React.ReactNode; description?: string; }) { return ( {icon} {title} {description && {description}} {children} ); } function FileInputWrapper({ label, placeholder, accept, onChange, preview, name, description, required = false, }: { label: string; placeholder?: string; accept?: string; onChange: (file: File | null) => void; preview?: string | null; name: string; description?: string; required?: boolean; }) { return ( {label} {required && ( * )} {description && ( {description} )} onChange(f)} leftSection={} aria-label={label} name={name} clearable={true} /> {preview ? (
Preview: {/* If preview is an image it will show; pdf preview might not render as image */} {/* Use or depending on file type — keep simple here */}
{`${label}
) : null} ); }