upd: tambah surat

deskripsi:
- layout form
- seeder category pelayanan
- api tambah surat

No Issues
This commit is contained in:
2025-12-18 17:42:25 +08:00
parent c897063eb5
commit 55fbf4836d
6 changed files with 657 additions and 180 deletions

View File

@@ -1,6 +1,8 @@
import FullScreenLoading from "@/components/FullScreenLoading";
import notification from "@/components/notificationGlobal";
import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
import apiFetch from "@/lib/apiFetch";
import { capitalizeWords, fromSlug, toSlug } from "@/server/lib/slug_converter";
import { fromSlug, toSlug } from "@/server/lib/slug_converter";
import {
ActionIcon,
Badge,
@@ -9,7 +11,8 @@ import {
Card,
Container,
Divider,
FileButton,
FileInput,
Flex,
Grid,
Group,
Select,
@@ -30,20 +33,22 @@ import { useLocation, useNavigate } from "react-router-dom";
import useSWR from "swr";
type DataItem = {
jenis: string;
key: string;
value: string;
};
type FormSurat = {
kategoryId: string;
kategoriId: string;
nama: string;
phone: string;
dataText: DataItem[];
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);
@@ -56,13 +61,25 @@ export default function FormSurat() {
const [formSurat, setFormSurat] = useState<FormSurat>({
nama: "",
phone: "",
kategoryId: "",
dataText: [],
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") {
@@ -86,18 +103,19 @@ export default function FormSurat() {
id: jenisSuratFix.id,
},
})
setDataSurat(get.data)
setFormSurat({
kategoryId: jenisSuratFix.id,
kategoriId: jenisSuratFix.id,
nama: "",
phone: "",
dataText: (get.data?.dataText || []).map((item: string) => ({
jenis: item,
dataPelengkap: (get.data?.dataPelengkap || []).map((item: { key: string }) => ({
key: item.key,
value: "",
})),
syaratDokumen: (get.data?.syaratDokumen || []).map(
(item: { name: string }) => ({
jenis: item.name,
(item: { key: string }) => ({
key: item.key,
value: "",
})
),
@@ -126,49 +144,129 @@ export default function FormSurat() {
}
}, [jenisSuratFix.id]);
function onSubmit() {
async function onSubmit() {
const isFormKosong = Object.values(formSurat).some((value) => {
if (Array.isArray(value)) {
return (
value.length === 0 ||
value.some((item) => !item.value?.trim())
);
value.some(
(item) => typeof item.value === "string" && item.value.trim() === ""
)
)
}
if (typeof value === "string") {
return value.trim() === "";
return value.trim() === ""
}
return false;
});
return false
})
if (isFormKosong) {
return notification({
title: "Gagal",
message: "Silahkan lengkapi form surat",
type: "error",
});
})
}
console.log("READY SUBMIT", formSurat);
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="apart" align="center">
<Group justify="space-between" align="center">
<Group align="center">
<IconBuildingCommunity size={28} />
<div>
<Text fw={800} size="xl">
Surat Keterangan Tidak Mampu (SKTM)
Layanan Pengajuan Surat Administrasi
</Text>
<Text size="sm" c="dimmed">
Blangko resmi untuk pengajuan Surat Keterangan Tidak Mampu
digunakan untuk keperluan pendidikan, kesehatan, atau
administrasi.
Formulir resmi untuk mengajukan berbagai jenis surat administrasi desa/kelurahan secara online.
</Text>
</div>
</Group>
@@ -188,11 +286,13 @@ export default function FormSurat() {
<TextInput
label={
<FieldLabel
label="Nama Lengkap"
hint="Nama lengkap pemohon"
label="Nama"
hint="Nama pemohon"
/>
}
placeholder="Budi Setiawan"
value={formSurat.nama}
onChange={(e) => validationForm({ key: "nama", value: e.target.value })}
/>
</Grid.Col>
@@ -205,6 +305,8 @@ export default function FormSurat() {
/>
}
placeholder="08123456789"
value={formSurat.phone}
onChange={(e) => validationForm({ key: "phone", value: e.target.value })}
/>
</Grid.Col>
@@ -227,7 +329,7 @@ export default function FormSurat() {
</FormSection>
{
jenisSuratFix.id != "" && dataSurat && dataSurat.dataText &&
jenisSuratFix.id != "" && dataSurat && dataSurat.dataPelengkap &&
<>
<FormSection
title="Data Pelengkap"
@@ -235,15 +337,18 @@ export default function FormSurat() {
>
<Grid>
{
dataSurat.dataText.map((item: any, index: number) => (
dataSurat.dataPelengkap.map((item: any, index: number) => (
<Grid.Col span={6} key={index}>
<TextInput
label={
<FieldLabel
label={dataSurat.dataText[index] == "nik" ? "NIK" : capitalizeWords(dataSurat.dataText[index])}
label={item.name}
hint={item.desc}
/>
}
placeholder={dataSurat.dataText[index] == "nik" ? "NIK" : capitalizeWords(dataSurat.dataText[index])}
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>
))
@@ -259,28 +364,13 @@ export default function FormSurat() {
{
dataSurat.syaratDokumen.map((item: any, index: number) => (
<Grid.Col span={6} key={index}>
<FieldLabelUpload
<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}
/>
<FileButton
onChange={async (file) => {
if (!file) return;
// const base64 = await fileToBase64(file);
// form.setFieldValue("foto", base64);
// setFotoName(file.name);
}}
accept="image/*"
>
{(props) => (
<Button
leftSection={<IconUpload size={16} />}
{...props}
mt="sm"
>
Upload File
</Button>
)}
</FileButton>
</Grid.Col>
))
}
@@ -319,36 +409,6 @@ function FieldLabel({ label, hint }: { label: string; hint?: string }) {
);
}
function FieldLabelUpload({
label,
description,
}: {
label: React.ReactNode;
description?: string;
}) {
return (
<div>
<Group justify="apart" style={{ width: "100%" }}>
<Group gap={6}>
<Text size="sm" fw={600}>
{label}
</Text>
{description && (
<ActionIcon size={18} variant="subtle" aria-hidden>
<IconInfoCircle size={16} />
</ActionIcon>
)}
</Group>
</Group>
{description && (
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
{description}
</Text>
)}
</div>
);
}
function FormSection({
title,
icon,
@@ -375,3 +435,62 @@ function FormSection({
</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>
);
}