upd: tambah surat
deskripsi: - layout form - seeder category pelayanan - api tambah surat No Issues
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user