upd: update data pelayanan surat
Deskripsi - form pencarian - detail data pengajuan - api - belom selesai submit NO Issues
This commit is contained in:
@@ -10,6 +10,7 @@ import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
|
|||||||
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
|
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
|
||||||
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
|
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
|
||||||
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
|
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
|
||||||
|
import UpdateDataSurat from "./pages/darmasaba/update_data_surat";
|
||||||
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
|
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
|
||||||
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
|
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
|
||||||
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
|
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
|
||||||
@@ -69,6 +70,10 @@ export default function AppRoutes() {
|
|||||||
path="/darmasaba/surat-keterangan-domisili-organisasi"
|
path="/darmasaba/surat-keterangan-domisili-organisasi"
|
||||||
element={<FormSuratKeteranganDomisiliOrganisasi />}
|
element={<FormSuratKeteranganDomisiliOrganisasi />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/update-data-surat"
|
||||||
|
element={<UpdateDataSurat />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/darmasaba/surat-keterangan-belum-kawin"
|
path="/darmasaba/surat-keterangan-belum-kawin"
|
||||||
element={<FormSuratKeteranganBelumKawin />}
|
element={<FormSuratKeteranganBelumKawin />}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const clientRoutes = {
|
|||||||
"/darmasaba/laporan-sampah": "/darmasaba/laporan-sampah",
|
"/darmasaba/laporan-sampah": "/darmasaba/laporan-sampah",
|
||||||
"/darmasaba/surat-keterangan-penghasilan": "/darmasaba/surat-keterangan-penghasilan",
|
"/darmasaba/surat-keterangan-penghasilan": "/darmasaba/surat-keterangan-penghasilan",
|
||||||
"/darmasaba/surat-keterangan-domisili-organisasi": "/darmasaba/surat-keterangan-domisili-organisasi",
|
"/darmasaba/surat-keterangan-domisili-organisasi": "/darmasaba/surat-keterangan-domisili-organisasi",
|
||||||
|
"/darmasaba/update-data-surat": "/darmasaba/update-data-surat",
|
||||||
"/darmasaba/surat-keterangan-belum-kawin": "/darmasaba/surat-keterangan-belum-kawin",
|
"/darmasaba/surat-keterangan-belum-kawin": "/darmasaba/surat-keterangan-belum-kawin",
|
||||||
"/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran",
|
"/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran",
|
||||||
"/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha",
|
"/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha",
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
import { Center, Loader, Overlay, Stack, Text } from "@mantine/core"
|
import { Center, Loader, Overlay, Stack, Text } from "@mantine/core";
|
||||||
|
|
||||||
type FullScreenLoadingProps = {
|
type FullScreenLoadingProps = {
|
||||||
visible: boolean
|
visible: boolean;
|
||||||
text?: string
|
text?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function FullScreenLoading({
|
export default function FullScreenLoading({
|
||||||
visible,
|
visible,
|
||||||
text = "Memproses data..."
|
text = "Memproses data...",
|
||||||
}: FullScreenLoadingProps) {
|
}: FullScreenLoadingProps) {
|
||||||
if (!visible) return null
|
if (!visible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay fixed blur={6} backgroundOpacity={0.3} zIndex={10000}>
|
||||||
fixed
|
<Center h="100%">
|
||||||
blur={6}
|
<Stack align="center" justify="center">
|
||||||
backgroundOpacity={0.3}
|
<Loader size="lg" />
|
||||||
zIndex={10000}
|
<Text size="sm" c="dimmed">
|
||||||
>
|
{text}
|
||||||
<Center h="100%">
|
</Text>
|
||||||
<Stack align="center" justify="center">
|
</Stack>
|
||||||
<Loader size="lg" />
|
</Center>
|
||||||
<Text size="sm" c="dimmed">
|
</Overlay>
|
||||||
{text}
|
);
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
</Overlay>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const overlayStyle: React.CSSProperties = {
|
const overlayStyle: React.CSSProperties = {
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
backdropFilter: "blur(6px)",
|
backdropFilter: "blur(6px)",
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.6)"
|
backgroundColor: "rgba(255, 255, 255, 0.6)",
|
||||||
}
|
};
|
||||||
|
|
||||||
const contentStyle: React.CSSProperties = {
|
const contentStyle: React.CSSProperties = {
|
||||||
flexDirection: "column"
|
flexDirection: "column",
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,50 +1,43 @@
|
|||||||
import { Badge, Button, Card, Center, Stack, Text, Title } from "@mantine/core"
|
import { Badge, Button, Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconCheck } from "@tabler/icons-react"
|
import { IconCheck } from "@tabler/icons-react";
|
||||||
|
|
||||||
type SuccessPengajuanProps = {
|
type SuccessPengajuanProps = {
|
||||||
noPengajuan: string
|
noPengajuan: string;
|
||||||
onClose?: () => void
|
onClose?: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function SuccessPengajuan({
|
export default function SuccessPengajuan({
|
||||||
noPengajuan,
|
noPengajuan,
|
||||||
onClose
|
onClose,
|
||||||
}: SuccessPengajuanProps) {
|
}: SuccessPengajuanProps) {
|
||||||
return (
|
return (
|
||||||
<Center h="100vh">
|
<Center h="100vh">
|
||||||
<Card
|
<Card shadow="md" radius="md" p="xl" withBorder maw={520} w="100%">
|
||||||
shadow="md"
|
<Stack align="center" gap="md">
|
||||||
radius="md"
|
<IconCheck size={56} color="green" />
|
||||||
p="xl"
|
|
||||||
withBorder
|
|
||||||
maw={520}
|
|
||||||
w="100%"
|
|
||||||
>
|
|
||||||
<Stack align="center" gap="md">
|
|
||||||
<IconCheck size={56} color="green" />
|
|
||||||
|
|
||||||
<Title order={3} ta="center">
|
<Title order={3} ta="center">
|
||||||
Pengajuan Berhasil Dibuat
|
Pengajuan Berhasil Dibuat
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Text ta="center" size="sm" c="dimmed">
|
<Text ta="center" size="sm" c="dimmed">
|
||||||
Pengajuan layanan surat sudah dibuat dengan nomor:
|
Pengajuan layanan surat sudah dibuat dengan nomor:
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Badge size="xl" variant="light" color="green">
|
<Badge size="xl" variant="light" color="green">
|
||||||
{noPengajuan}
|
{noPengajuan}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Text ta="center" size="sm">
|
<Text ta="center" size="sm">
|
||||||
Nomor ini akan digunakan untuk mengakses dan memantau status
|
Nomor ini akan digunakan untuk mengakses dan memantau status
|
||||||
pengajuan surat Anda.
|
pengajuan surat Anda.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button fullWidth mt="md" onClick={onClose}>
|
<Button fullWidth mt="md" onClick={onClose}>
|
||||||
Selesai
|
Selesai
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Center>
|
</Center>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,493 +4,510 @@ import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
|
|||||||
import apiFetch from "@/lib/apiFetch";
|
import apiFetch from "@/lib/apiFetch";
|
||||||
import { fromSlug, toSlug } from "@/server/lib/slug_converter";
|
import { fromSlug, toSlug } from "@/server/lib/slug_converter";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
FileInput,
|
FileInput,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Tooltip
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
IconBuildingCommunity,
|
IconBuildingCommunity,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconUpload,
|
IconUpload,
|
||||||
IconUser
|
IconUser,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
type DataItem = {
|
type DataItem = {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormSurat = {
|
type FormSurat = {
|
||||||
kategoriId: string;
|
kategoriId: string;
|
||||||
nama: string;
|
nama: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
dataPelengkap: DataItem[];
|
dataPelengkap: DataItem[];
|
||||||
syaratDokumen: DataItem[];
|
syaratDokumen: DataItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function FormSurat() {
|
export default function FormSurat() {
|
||||||
const [noPengajuan, setNoPengajuan] = useState("")
|
const [noPengajuan, setNoPengajuan] = useState("");
|
||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
const jenisSurat = query.get("jenis");
|
const jenisSurat = query.get("jenis");
|
||||||
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
|
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
|
||||||
apiFetch.api.pelayanan.category.get(),
|
apiFetch.api.pelayanan.category.get(),
|
||||||
);
|
);
|
||||||
const [jenisSuratFix, setJenisSuratFix] = useState({ name: "", id: "" });
|
const [jenisSuratFix, setJenisSuratFix] = useState({ name: "", id: "" });
|
||||||
const [dataSurat, setDataSurat] = useState<any>({})
|
const [dataSurat, setDataSurat] = useState<any>({});
|
||||||
const [formSurat, setFormSurat] = useState<FormSurat>({
|
const [formSurat, setFormSurat] = useState<FormSurat>({
|
||||||
|
nama: "",
|
||||||
|
phone: "",
|
||||||
|
kategoriId: "",
|
||||||
|
dataPelengkap: [],
|
||||||
|
syaratDokumen: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const listCategory = data?.data || [];
|
||||||
|
|
||||||
|
function onResetAll() {
|
||||||
|
setNoPengajuan("");
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setFormSurat({
|
||||||
nama: "",
|
nama: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
kategoriId: "",
|
kategoriId: "",
|
||||||
dataPelengkap: [],
|
dataPelengkap: [],
|
||||||
syaratDokumen: [],
|
syaratDokumen: [],
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const listCategory = data?.data || [];
|
function onGetJenisSurat() {
|
||||||
|
try {
|
||||||
function onResetAll() {
|
if (!jenisSurat || jenisSurat == "null") {
|
||||||
setNoPengajuan("")
|
setJenisSuratFix({ name: "", id: "" });
|
||||||
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 {
|
} else {
|
||||||
setFormSurat({
|
const namaJenis = fromSlug(jenisSurat);
|
||||||
...formSurat,
|
const data = listCategory.find((item: any) => item.name == namaJenis);
|
||||||
[key]: value,
|
if (!data) return;
|
||||||
})
|
setJenisSuratFix(data);
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
async function getDetailJenisSurat() {
|
||||||
<Container size="md" w={"100%"}>
|
try {
|
||||||
<FullScreenLoading visible={submitLoading} />
|
const get: any = await apiFetch.api.pelayanan.category.detail.get({
|
||||||
{
|
query: {
|
||||||
noPengajuan != "" &&
|
id: jenisSuratFix.id,
|
||||||
<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}>
|
setDataSurat(get.data);
|
||||||
<TextInput
|
setFormSurat({
|
||||||
label={
|
kategoriId: jenisSuratFix.id,
|
||||||
<FieldLabel
|
nama: "",
|
||||||
label="Nomor Telephone"
|
phone: "",
|
||||||
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
|
dataPelengkap: (get.data?.dataPelengkap || []).map(
|
||||||
/>
|
(item: { key: string }) => ({
|
||||||
}
|
key: item.key,
|
||||||
placeholder="08123456789"
|
value: "",
|
||||||
value={formSurat.phone}
|
}),
|
||||||
onChange={(e) => validationForm({ key: "phone", value: e.target.value })}
|
),
|
||||||
/>
|
syaratDokumen: (get.data?.syaratDokumen || []).map(
|
||||||
</Grid.Col>
|
(item: { key: string }) => ({
|
||||||
|
key: item.key,
|
||||||
|
value: "",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<Grid.Col span={12}>
|
useShallowEffect(() => {
|
||||||
<Select
|
mutate();
|
||||||
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>
|
|
||||||
|
|
||||||
{
|
useShallowEffect(() => {
|
||||||
jenisSuratFix.id != "" && dataSurat && dataSurat.dataPelengkap &&
|
if (listCategory.length > 0) {
|
||||||
<>
|
onGetJenisSurat();
|
||||||
<FormSection
|
}
|
||||||
title="Data Pelengkap"
|
}, [jenisSurat, listCategory]);
|
||||||
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
|
useShallowEffect(() => {
|
||||||
title="Syarat Dokumen"
|
if (jenisSuratFix.id != "") {
|
||||||
description="Syarat dokumen yang diperlukan"
|
getDetailJenisSurat();
|
||||||
>
|
}
|
||||||
<Grid>
|
}, [jenisSuratFix.id]);
|
||||||
{
|
|
||||||
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 */}
|
async function onSubmit() {
|
||||||
<Group justify="right" mt="md">
|
const isFormKosong = Object.values(formSurat).some((value) => {
|
||||||
<Button variant="default" onClick={() => { }}>
|
if (Array.isArray(value)) {
|
||||||
Reset
|
return (
|
||||||
</Button>
|
value.length === 0 ||
|
||||||
<Button onClick={onSubmit}>Kirim</Button>
|
value.some(
|
||||||
</Group>
|
(item) =>
|
||||||
</>
|
typeof item.value === "string" && item.value.trim() === "",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
if (typeof value === "string") {
|
||||||
</Stack>
|
return value.trim() === "";
|
||||||
</Stack>
|
}
|
||||||
</Box>
|
|
||||||
</Container>
|
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 }) {
|
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||||
return (
|
return (
|
||||||
<Group justify="apart" gap="xs" align="center">
|
<Group justify="apart" gap="xs" align="center">
|
||||||
<Text fw={600}>{label}</Text>
|
<Text fw={600}>{label}</Text>
|
||||||
{hint && (
|
{hint && (
|
||||||
<Tooltip label={hint} withArrow>
|
<Tooltip label={hint} withArrow>
|
||||||
<ActionIcon size={24} variant="subtle">
|
<ActionIcon size={24} variant="subtle">
|
||||||
<IconInfoCircle size={16} />
|
<IconInfoCircle size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormSection({
|
function FormSection({
|
||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
children,
|
children,
|
||||||
description,
|
description,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
description?: string;
|
description?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card radius="md" shadow="sm" withBorder>
|
<Card radius="md" shadow="sm" withBorder>
|
||||||
<Group justify="apart" align="center" mb="xs">
|
<Group justify="apart" align="center" mb="xs">
|
||||||
<Group align="center" gap="xs">
|
<Group align="center" gap="xs">
|
||||||
{icon}
|
{icon}
|
||||||
<Text fw={700}>{title}</Text>
|
<Text fw={700}>{title}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
{description && <Badge variant="light">{description}</Badge>}
|
{description && <Badge variant="light">{description}</Badge>}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Divider mb="sm" />
|
<Divider mb="sm" />
|
||||||
<Stack gap="sm">{children}</Stack>
|
<Stack gap="sm">{children}</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileInputWrapper({
|
function FileInputWrapper({
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
accept,
|
accept,
|
||||||
onChange,
|
onChange,
|
||||||
preview,
|
preview,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
accept?: string;
|
accept?: string;
|
||||||
onChange: (file: File | null) => void;
|
onChange: (file: File | null) => void;
|
||||||
preview?: string | null;
|
preview?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Flex direction={"column"}>
|
<Flex direction={"column"}>
|
||||||
<Group justify="apart" align="center">
|
<Group justify="apart" align="center">
|
||||||
<Text fw={500}>{label}</Text>
|
<Text fw={500}>{label}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
{description && (
|
{description && (
|
||||||
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
|
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<FileInput
|
<FileInput
|
||||||
accept={accept}
|
accept={accept}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(f) => onChange(f)}
|
onChange={(f) => onChange(f)}
|
||||||
leftSection={<IconUpload />}
|
leftSection={<IconUpload />}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
name={name}
|
name={name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{preview ? (
|
{preview ? (
|
||||||
<div>
|
<div>
|
||||||
<Text size="xs" color="dimmed">
|
<Text size="xs" color="dimmed">
|
||||||
Preview:
|
Preview:
|
||||||
</Text>
|
</Text>
|
||||||
{/* If preview is an image it will show; pdf preview might not render as image */}
|
{/* 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 */}
|
{/* Use <object> or <img> depending on file type — keep simple here */}
|
||||||
<div style={{ marginTop: 6 }}>
|
<div style={{ marginTop: 6 }}>
|
||||||
<img
|
<img
|
||||||
src={preview}
|
src={preview}
|
||||||
alt={`${label} preview`}
|
alt={`${label} preview`}
|
||||||
style={{ maxWidth: "200px", borderRadius: 4 }}
|
style={{ maxWidth: "200px", borderRadius: 4 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
474
src/pages/darmasaba/update_data_surat.tsx
Normal file
474
src/pages/darmasaba/update_data_surat.tsx
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
import ModalFile from "@/components/ModalFile";
|
||||||
|
import notification from "@/components/notificationGlobal";
|
||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Alert,
|
||||||
|
Anchor,
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
FileInput,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Tooltip
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { 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 [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
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
(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={() => console.log('Submit clicked')}>Kirim</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -215,7 +215,7 @@ const PelayananRoute = new Elysia({
|
|||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
syaratDokumen: true,
|
syaratDokumen: true,
|
||||||
dataPelengkap:true
|
dataPelengkap: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Warga: {
|
Warga: {
|
||||||
@@ -575,7 +575,7 @@ const PelayananRoute = new Elysia({
|
|||||||
CategoryPelayanan: {
|
CategoryPelayanan: {
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
dataText: true,
|
dataPelengkap: true,
|
||||||
syaratDokumen: true,
|
syaratDokumen: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -595,7 +595,15 @@ const PelayananRoute = new Elysia({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return { success: false, message: "Data tidak ditemukan" }
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Data tidak ditemukan",
|
||||||
|
pengajuan: {},
|
||||||
|
history: [],
|
||||||
|
warga: {},
|
||||||
|
syaratDokumen: [],
|
||||||
|
dataPelengkap: [],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSurat = await prisma.suratPelayanan.findFirst({
|
const dataSurat = await prisma.suratPelayanan.findFirst({
|
||||||
@@ -621,7 +629,7 @@ const PelayananRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const dataText = await prisma.dataTextPelayanan.findMany({
|
const dataPelengkap = await prisma.dataTextPelayanan.findMany({
|
||||||
where: {
|
where: {
|
||||||
idPengajuanLayanan: data?.id,
|
idPengajuanLayanan: data?.id,
|
||||||
isActive: true
|
isActive: true
|
||||||
@@ -635,23 +643,37 @@ const PelayananRoute = new Elysia({
|
|||||||
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
|
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
|
||||||
name: string;
|
name: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
|
key: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
const dataSyaratFix = dataSyarat.map((item) => {
|
const dataSyaratFix = dataSyarat.map((item) => {
|
||||||
// const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc
|
const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc
|
||||||
|
const name = syaratDokumen.find((v) => v.key == item.jenis)?.name
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
jenis: item.jenis,
|
key: item.jenis,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
|
name: name ?? '',
|
||||||
|
desc: desc ?? ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const dataTextFix = dataText.map((item) => {
|
const dataPelengkapList = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
|
||||||
// const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis)
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
key: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
const dataTextFix = dataPelengkap.map((item) => {
|
||||||
|
const ini = dataPelengkapList.find((v) => v.key == item.jenis)
|
||||||
|
const desc = ini?.desc
|
||||||
|
const name = ini?.name
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
jenis: item.jenis,
|
key: item.jenis,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
|
desc: desc ?? '',
|
||||||
|
name: name ?? ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -702,11 +724,13 @@ const PelayananRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const datafix = {
|
const datafix = {
|
||||||
|
success: true,
|
||||||
|
message: 'sukses',
|
||||||
pengajuan: dataPengajuan,
|
pengajuan: dataPengajuan,
|
||||||
history: dataHistoryFix,
|
history: dataHistoryFix,
|
||||||
warga: warga,
|
warga: warga,
|
||||||
syaratDokumen: dataSyaratFix,
|
syaratDokumen: dataSyaratFix,
|
||||||
dataText: dataTextFix,
|
dataPelengkap: dataTextFix,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -786,12 +810,12 @@ const PelayananRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post("/update", async ({ body }) => {
|
.post("/update", async ({ body }) => {
|
||||||
const { nomerPengajuan, syaratDokumen, dataText } = body
|
const { id, syaratDokumen, dataPelengkap } = body
|
||||||
let dataUpdate = []
|
let dataUpdate = []
|
||||||
|
|
||||||
const pengajuan = await prisma.pelayananAjuan.findFirst({
|
const pengajuan = await prisma.pelayananAjuan.findUnique({
|
||||||
where: {
|
where: {
|
||||||
noPengajuan: nomerPengajuan,
|
id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -804,29 +828,16 @@ const PelayananRoute = new Elysia({
|
|||||||
return { success: false, message: 'pengajuan surat tidak dapat diupdate karena status ' + pengajuan.status }
|
return { success: false, message: 'pengajuan surat tidak dapat diupdate karena status ' + pengajuan.status }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataText && dataText.length > 0) {
|
if (dataPelengkap && dataPelengkap.length > 0) {
|
||||||
for (const item of dataText) {
|
for (const item of dataPelengkap) {
|
||||||
dataUpdate.push(item.jenis)
|
dataUpdate.push(item.key)
|
||||||
const hasil = await prisma.dataTextPelayanan.findFirst({
|
|
||||||
where: {
|
|
||||||
idPengajuanLayanan: pengajuan.id,
|
|
||||||
jenis: item.jenis,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const upd = await prisma.dataTextPelayanan.update({
|
||||||
const upd = await prisma.dataTextPelayanan.upsert({
|
|
||||||
where: {
|
where: {
|
||||||
id: hasil?.id
|
id: item.id
|
||||||
},
|
},
|
||||||
update: {
|
data: {
|
||||||
value: item.value,
|
value: item.value,
|
||||||
},
|
|
||||||
create: {
|
|
||||||
value: item.value,
|
|
||||||
jenis: item.jenis,
|
|
||||||
idPengajuanLayanan: pengajuan.id,
|
|
||||||
idCategory: pengajuan.idCategory,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -849,34 +860,15 @@ const PelayananRoute = new Elysia({
|
|||||||
|
|
||||||
if (syaratDokumen && syaratDokumen.length > 0) {
|
if (syaratDokumen && syaratDokumen.length > 0) {
|
||||||
for (const item of syaratDokumen) {
|
for (const item of syaratDokumen) {
|
||||||
const pilih = syarat?.find((cat) => cat.desc.toLowerCase() == item.jenis.toLowerCase() || cat.name.toLowerCase() == item.jenis.toLowerCase())?.name;
|
dataUpdate.push(item.key)
|
||||||
dataUpdate.push(pilih)
|
const upd = await prisma.syaratDokumenPelayanan.update({
|
||||||
|
|
||||||
const hasil = await prisma.syaratDokumenPelayanan.findFirst({
|
|
||||||
where: {
|
where: {
|
||||||
idPengajuanLayanan: pengajuan.id,
|
id: item.id
|
||||||
jenis: pilih,
|
},
|
||||||
|
data: {
|
||||||
|
value: item.value,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (hasil && hasil.id) {
|
|
||||||
const upd = await prisma.syaratDokumenPelayanan.upsert({
|
|
||||||
where: {
|
|
||||||
id: hasil.id
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
value: item.value,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
value: item.value,
|
|
||||||
jenis: hasil.jenis,
|
|
||||||
idPengajuanLayanan: pengajuan.id,
|
|
||||||
idCategory: pengajuan.idCategory,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return { success: false, message: 'dokumen tidak dapat diupload' }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,16 +898,20 @@ const PelayananRoute = new Elysia({
|
|||||||
|
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
nomerPengajuan: t.String({
|
id: t.String({
|
||||||
error: "nomer pengajuan harus diisi",
|
error: "id harus diisi",
|
||||||
description: "Nomer pengajuan yang ingin diupdate"
|
description: "ID yang ingin diupdate"
|
||||||
}),
|
}),
|
||||||
dataText: t.Optional(t.Array(
|
dataPelengkap: t.Optional(t.Array(
|
||||||
t.Object({
|
t.Object({
|
||||||
jenis: t.String({
|
id: t.String({
|
||||||
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
|
description: "ID Data Pelengkap.",
|
||||||
|
error: "id harus diisi"
|
||||||
|
}),
|
||||||
|
key: t.String({
|
||||||
|
description: "Key field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
|
||||||
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
|
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
|
||||||
error: "jenis harus diisi"
|
error: "key harus diisi"
|
||||||
}),
|
}),
|
||||||
value: t.String({
|
value: t.String({
|
||||||
description: "Isi atau nilai dari jenis field terkait.",
|
description: "Isi atau nilai dari jenis field terkait.",
|
||||||
@@ -927,26 +923,30 @@ const PelayananRoute = new Elysia({
|
|||||||
description: "Kumpulan data text dinamis sesuai kategori layanan.",
|
description: "Kumpulan data text dinamis sesuai kategori layanan.",
|
||||||
examples: [
|
examples: [
|
||||||
[
|
[
|
||||||
{ jenis: "nama", value: "Budi Santoso" },
|
{ id: "1", key: "nama", value: "Budi Santoso" },
|
||||||
{ jenis: "jenis kelamin", value: "Laki-laki" },
|
{ id: "2", key: "jenis kelamin", value: "Laki-laki" },
|
||||||
{ jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
|
{ id: "3", key: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
|
||||||
{ jenis: "negara", value: "Indonesia" },
|
{ id: "4", key: "negara", value: "Indonesia" },
|
||||||
{ jenis: "agama", value: "Islam" },
|
{ id: "5", key: "agama", value: "Islam" },
|
||||||
{ jenis: "status perkawinan", value: "Belum menikah" },
|
{ id: "6", key: "status perkawinan", value: "Belum menikah" },
|
||||||
{ jenis: "alamat", value: "Jl. Mawar No. 10" },
|
{ id: "7", key: "alamat", value: "Jl. Mawar No. 10" },
|
||||||
{ jenis: "pekerjaan", value: "Karyawan Swasta" },
|
{ id: "8", key: "pekerjaan", value: "Karyawan Swasta" },
|
||||||
{ jenis: "jenis usaha", value: "usaha makanan" },
|
{ id: "9", key: "jenis usaha", value: "usaha makanan" },
|
||||||
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" },
|
{ id: "10", key: "alamat usaha", value: "Jl. Melati No. 21" },
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
syaratDokumen: t.Optional(t.Array(
|
syaratDokumen: t.Optional(t.Array(
|
||||||
t.Object({
|
t.Object({
|
||||||
jenis: t.String({
|
id: t.String({
|
||||||
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
|
description: "ID syarat dokumen",
|
||||||
|
error: "id harus diisi"
|
||||||
|
}),
|
||||||
|
key: t.String({
|
||||||
|
description: "Key dokumen persyaratan yang diminta oleh kategori layanan.",
|
||||||
examples: ["ktp", "kk", "surat_pengantar_rt"],
|
examples: ["ktp", "kk", "surat_pengantar_rt"],
|
||||||
error: "jenis harus diisi"
|
error: "key harus diisi"
|
||||||
}),
|
}),
|
||||||
value: t.String({
|
value: t.String({
|
||||||
description: "Nama file atau identifier file dokumen yang diupload.",
|
description: "Nama file atau identifier file dokumen yang diupload.",
|
||||||
@@ -958,9 +958,9 @@ const PelayananRoute = new Elysia({
|
|||||||
description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.",
|
description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.",
|
||||||
examples: [
|
examples: [
|
||||||
[
|
[
|
||||||
{ jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
|
{ id: "1", key: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
|
||||||
{ jenis: "ktp/kk", value: "kk_budi.png" },
|
{ id: "2", key: "ktp/kk", value: "kk_budi.png" },
|
||||||
{ jenis: "foto lokasi", value: "foto_lokasi_budi.png" }
|
{ id: "3", key: "foto lokasi", value: "foto_lokasi_budi.png" }
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -969,7 +969,6 @@ const PelayananRoute = new Elysia({
|
|||||||
detail: {
|
detail: {
|
||||||
summary: "Update Data Pengajuan Pelayanan Surat",
|
summary: "Update Data Pengajuan Pelayanan Surat",
|
||||||
description: `tool untuk update data pengajuan pelayanan surat`,
|
description: `tool untuk update data pengajuan pelayanan surat`,
|
||||||
tags: ["mcp"]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get("/list", async ({ query }) => {
|
.get("/list", async ({ query }) => {
|
||||||
@@ -1116,5 +1115,47 @@ const PelayananRoute = new Elysia({
|
|||||||
description: `tool untuk mendapatkan jumlah pengajuan pelayanan surat warga`,
|
description: `tool untuk mendapatkan jumlah pengajuan pelayanan surat warga`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.post("/get-no-pengajuan", async ({ body }) => {
|
||||||
|
const { phone, noPengajuan } = body;
|
||||||
|
|
||||||
|
if (!isValidPhone(phone)) {
|
||||||
|
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const nomorHP = normalizePhoneNumber({ phone: phone })
|
||||||
|
const data = await prisma.pelayananAjuan.findMany({
|
||||||
|
where: {
|
||||||
|
noPengajuan: noPengajuan,
|
||||||
|
Warga: {
|
||||||
|
phone: nomorHP
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
noPengajuan: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length == 0) {
|
||||||
|
return { success: false, message: 'Data pengajuan tidak ditemukan' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
nomer: noPengajuan
|
||||||
|
};
|
||||||
|
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
phone: t.String({ minLength: 1, error: "Nomor telepon harus diisi" }),
|
||||||
|
noPengajuan: t.String({ minLength: 1, error: "Nomor pengajuan harus diisi" }),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Cek Nomor Pengajuan Surat",
|
||||||
|
description: `tool untuk memeriksa apakah nomor pengajuan surat valid dan terkait dengan nomor telepon warga. Jika valid, mengembalikan nomor pengajuan.`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default PelayananRoute
|
export default PelayananRoute
|
||||||
|
|||||||
Reference in New Issue
Block a user