diff --git a/src/components/KategoriPelayananSurat.tsx b/src/components/KategoriPelayananSurat.tsx index d7aa695..afb546a 100644 --- a/src/components/KategoriPelayananSurat.tsx +++ b/src/components/KategoriPelayananSurat.tsx @@ -11,10 +11,9 @@ import { Modal, Stack, Table, - TagsInput, Text, Title, - Tooltip, + Tooltip } from "@mantine/core"; import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react"; @@ -44,13 +43,13 @@ export default function KategoriPelayananSurat({ const [dataChoose, setDataChoose] = useState({ id: "", name: "", - syaratDokumen: [{ name: "", desc: "" }], - dataText: [""], + syaratDokumen: [{ key: "", name: "", desc: "" }], + dataPelengkap: [{ key: "", name: "", desc: "" }], }); const [dataTambah, setDataTambah] = useState({ name: "", - syaratDokumen: [{ name: "", desc: "" }], - dataText: [""], + syaratDokumen: [{ key: "", name: "", desc: "" }], + dataPelengkap: [{ key: "", name: "", desc: "" }], }); useShallowEffect(() => { @@ -60,8 +59,8 @@ export default function KategoriPelayananSurat({ async function handleCreate() { try { setBtnLoading(true); - const cleanedDataText = dataTambah.dataText - .map((v) => v.trim()) + const cleanedDataText = dataTambah.dataPelengkap + .map((v) => v.name.trim()) .filter((v) => v !== ""); const cleanedSyarat = dataTambah.syaratDokumen .map((item) => ({ @@ -82,8 +81,8 @@ export default function KategoriPelayananSurat({ closeTambah(); setDataTambah({ name: "", - syaratDokumen: [{ name: "", desc: "" }], - dataText: [""], + syaratDokumen: [{ key: "", name: "", desc: "" }], + dataPelengkap: [{ key: "", name: "", desc: "" }], }); notification({ title: "Success", @@ -112,8 +111,8 @@ export default function KategoriPelayananSurat({ async function handleEdit() { try { setBtnLoading(true); - const cleanedDataText = dataChoose.dataText - .map((v) => v.trim()) + const cleanedDataText = dataChoose.dataPelengkap + .map((v) => v.name.trim()) .filter((v) => v !== ""); const cleanedSyarat = dataChoose.syaratDokumen .map((item) => ({ @@ -191,7 +190,7 @@ export default function KategoriPelayananSurat({ function handleAddSyarat() { setDataChoose({ ...dataChoose, - syaratDokumen: [...dataChoose.syaratDokumen, { name: "", desc: "" }], + syaratDokumen: [...dataChoose.syaratDokumen, { key: "", name: "", desc: "" }], }); } @@ -204,7 +203,7 @@ export default function KategoriPelayananSurat({ function handleEditSyarat( index: number, - data: { name: string; desc: string }, + data: { key: string; name: string; desc: string }, ) { setDataChoose({ ...dataChoose, @@ -233,15 +232,15 @@ export default function KategoriPelayananSurat({ } /> - - setDataChoose({ ...dataChoose, dataText: value }) + setDataChoose({ ...dataChoose, dataPelengkap: value }) } - /> + /> */} @@ -295,6 +294,7 @@ export default function KategoriPelayananSurat({ value={v.name} onChange={(e) => handleEditSyarat(i, { + key: v.key, name: e.target.value, desc: v.desc, }) @@ -308,6 +308,7 @@ export default function KategoriPelayananSurat({ value={v.desc} onChange={(e) => handleEditSyarat(i, { + key: v.key, name: v.name, desc: e.target.value, }) @@ -347,7 +348,7 @@ export default function KategoriPelayananSurat({ } /> - setDataTambah({ ...dataTambah, dataText: value }) } - /> + /> */} @@ -372,7 +373,7 @@ export default function KategoriPelayananSurat({ ...dataTambah, syaratDokumen: [ ...dataTambah.syaratDokumen, - { name: "", desc: "" }, + { key: "", name: "", desc: "" }, ], }); }} @@ -524,8 +525,8 @@ export default function KategoriPelayananSurat({ Data Pelengkap - {dataChoose?.dataText?.map((v: any) => ( - {v} + {dataChoose?.dataPelengkap?.map((v: any) => ( + {v.name} ))} diff --git a/src/components/SuccessPengajuanSurat.tsx b/src/components/SuccessPengajuanSurat.tsx index 02165ec..9a21128 100644 --- a/src/components/SuccessPengajuanSurat.tsx +++ b/src/components/SuccessPengajuanSurat.tsx @@ -4,11 +4,13 @@ import { IconCheck } from "@tabler/icons-react"; type SuccessPengajuanProps = { noPengajuan: string; onClose?: () => void; + category?: 'create' | 'update'; }; export default function SuccessPengajuan({ noPengajuan, onClose, + category }: SuccessPengajuanProps) { return (
@@ -17,11 +19,11 @@ export default function SuccessPengajuan({ - Pengajuan Berhasil Dibuat + {category == 'create' ? 'Pengajuan Berhasil Dibuat' : 'Pengajuan Berhasil Diupdate'} - Pengajuan layanan surat sudah dibuat dengan nomor: + {category == 'create' ? 'Pengajuan layanan surat sudah dibuat dengan nomor:' : 'Pengajuan layanan surat sudah diupdate dengan nomor:'} diff --git a/src/pages/darmasaba/surat.tsx b/src/pages/darmasaba/surat.tsx index 50a22e9..167dda6 100644 --- a/src/pages/darmasaba/surat.tsx +++ b/src/pages/darmasaba/surat.tsx @@ -15,13 +15,14 @@ import { Flex, Grid, Group, + Modal, Select, Stack, Text, TextInput, Tooltip, } from "@mantine/core"; -import { useShallowEffect } from "@mantine/hooks"; +import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconBuildingCommunity, IconInfoCircle, @@ -46,6 +47,7 @@ type FormSurat = { }; export default function FormSurat() { + const [opened, { open, close }] = useDisclosure(false); const [noPengajuan, setNoPengajuan] = useState(""); const [submitLoading, setSubmitLoading] = useState(false); const navigate = useNavigate(); @@ -141,7 +143,7 @@ export default function FormSurat() { } }, [jenisSuratFix.id]); - async function onSubmit() { + function onChecking() { const isFormKosong = Object.values(formSurat).some((value) => { if (Array.isArray(value)) { return ( @@ -166,8 +168,12 @@ export default function FormSurat() { message: "Silahkan lengkapi form surat", type: "error", }); + } else { + open(); } + } + async function onSubmit() { try { setSubmitLoading(true); // 🔥 CLONE state SEKALI @@ -197,11 +203,7 @@ export default function FormSurat() { 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", - }); + setNoPengajuan(res.data?.noPengajuan || ""); } else { notification({ title: "Gagal", @@ -252,161 +254,192 @@ export default function FormSurat() { } return ( - + + + + + Apakah anda yakin ingin mengirim pengajuan surat ini? + + + + + + + - {noPengajuan != "" && ( + {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: 3 Sections - -
+ ) + : + - {/* Header Section */} - } - description="Informasi identitas pemohon" - > - - - } - placeholder="Budi Setiawan" - value={formSurat.nama} - onChange={(e) => - validationForm({ key: "nama", value: e.target.value }) - } - /> - + + + +
+ + Layanan Pengajuan Surat Administrasi + + + Formulir resmi untuk mengajukan berbagai jenis surat + administrasi desa/kelurahan secara online. + +
+
+ + Form Length: 3 Sections + +
+ + {/* Header Section */} + } + description="Informasi identitas pemohon" + > + + + } + placeholder="Budi Setiawan" + value={formSurat.nama} + onChange={(e) => + validationForm({ key: "nama", value: e.target.value }) + } + /> + - - - } - placeholder="08123456789" - value={formSurat.phone} - onChange={(e) => - validationForm({ key: "phone", value: e.target.value }) - } - /> - - - - - - ))} - - + } + 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); + }} + /> + +
+
- {/* Actions */} - - - - - - )} + */} + + + + )} +
- -
+ + } +
); } diff --git a/src/pages/darmasaba/update_data_surat.tsx b/src/pages/darmasaba/update_data_surat.tsx index f48833d..ee135a9 100644 --- a/src/pages/darmasaba/update_data_surat.tsx +++ b/src/pages/darmasaba/update_data_surat.tsx @@ -1,5 +1,7 @@ +import FullScreenLoading from "@/components/FullScreenLoading"; import ModalFile from "@/components/ModalFile"; import notification from "@/components/notificationGlobal"; +import SuccessPengajuan from "@/components/SuccessPengajuanSurat"; import apiFetch from "@/lib/apiFetch"; import { ActionIcon, @@ -15,12 +17,13 @@ import { Flex, Grid, Group, + Modal, Stack, Text, TextInput, Tooltip } from "@mantine/core"; -import { useShallowEffect } from "@mantine/hooks"; +import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconBuildingCommunity, IconInfoCircle, @@ -336,6 +339,10 @@ function SearchData() { function DataUpdate({ noPengajuan }: { noPengajuan: string }) { + const [opened, { open, close }] = useDisclosure(false) + const navigate = useNavigate() + const [sukses, setSukses] = useState(false) + const [submitLoading, setSubmitLoading] = useState(false) const [dataPelengkap, setDataPelengkap] = useState([]) const [dataSyaratDokumen, setDataSyaratDokumen] = useState([]) const [dataPengajuan, setDataPengajuan] = useState({}) @@ -406,69 +413,208 @@ function DataUpdate({ noPengajuan }: { noPengajuan: string }) { })); } + function updateArrayByKey( + list: UpdateDataItem[], + id: string, + value: any, + ): UpdateDataItem[] { + return list.map((item) => + item.id === id ? { ...item, value } : item, + ); + } + + function onChecking() { + if (formSurat.dataPelengkap.length == 0 && formSurat.syaratDokumen.length == 0) + return notification({ + title: "Peringatan", + message: "Tidak ada data yang diupdate", + type: "warning", + }); + const isFormKosong = Object.values(formSurat).some((value: UpdateDataItem[] | string) => { + if (Array.isArray(value)) { + return ( + value.some( + (item) => + typeof item.value === "string" && item.value.trim() === "", + ) + ); + } + + if (typeof value === "string") { + return value.trim() === ""; + } + + return false; + }); + + if (isFormKosong) { + return notification({ + title: "Gagal", + message: "Silahkan lengkapi form surat", + type: "error", + }); + } else { + open() + } + } + + async function onSubmit() { + try { + setSubmitLoading(true); + // 🔥 CLONE state SEKALI + let finalFormSurat = structuredClone(formSurat); + + // 2️⃣ Upload satu per satu + for (const itemUpload of finalFormSurat.syaratDokumen) { + const updImg = await apiFetch.api.pengaduan.upload.post({ + file: itemUpload.value, + folder: "syarat-dokumen", + }); + + if (updImg.status === 200) { + // 🔥 UPDATE OBJECT LOKAL (BUKAN STATE) + finalFormSurat.syaratDokumen = updateArrayByKey( + finalFormSurat.syaratDokumen, + itemUpload.id, + updImg.data?.filename || "", + ); + } + } + + // 3️⃣ SET STATE SEKALI (optional, untuk UI) + setFormSurat(finalFormSurat); + + // 4️⃣ SUBMIT KE API + const res = await apiFetch.api.pelayanan.update.post({ + id: dataPengajuan && ('id' in dataPengajuan) ? dataPengajuan.id : "", + dataPelengkap: finalFormSurat.dataPelengkap, + syaratDokumen: finalFormSurat.syaratDokumen, + }); + + if (res.status === 200) { + setSukses(true); + } else { + notification({ + title: "Gagal", + message: + "Pengajuan surat gagal dibuat, silahkan coba beberapa saat lagi", + type: "error", + }); + } + } catch (error) { + notification({ + title: "Gagal", + message: "Server Error", + type: "error", + }); + } finally { + setSubmitLoading(false); + } + } return ( <> + + + + + Apakah anda yakin ingin mengupdate pengajuan surat ini? + + + + + + + { - (status != "ditolak" && status != "antrian") - && ⚠} /> + sukses ? + { + navigate("/darmasaba/update-data-surat"); + }} + category="update" + /> + : + <> + { + (status != "ditolak" && status != "antrian") + && ⚠} /> + } + + + {dataPelengkap.map((item: any, index: number) => ( + + + } + 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"} + /> + + ))} + + + + + + {dataSyaratDokumen.map((item: any, index: number) => ( + + + validationForm({ + kategori: "syaratDokumen", + value: { id: item.id, key: item.key, value: file }, + }) + } + name={item.name} + disabled={status != "ditolak" && status != "antrian"} + /> + + ))} + + + + + + + } - - - {dataPelengkap.map((item: any, index: number) => ( - - - } - 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"} - /> - - ))} - - - - - - {dataSyaratDokumen.map((item: any, index: number) => ( - - - validationForm({ - kategori: "syaratDokumen", - value: { id: item.id, key: item.key, value: file }, - }) - } - name={item.name} - disabled={status != "ditolak" && status != "antrian"} - /> - - ))} - - - - - - ) } diff --git a/src/server/routes/pelayanan_surat_route.ts b/src/server/routes/pelayanan_surat_route.ts index c7839aa..cea18e1 100644 --- a/src/server/routes/pelayanan_surat_route.ts +++ b/src/server/routes/pelayanan_surat_route.ts @@ -5,6 +5,7 @@ import { getLastUpdated } from "../lib/get-last-updated" import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat" import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone" import { prisma } from "../lib/prisma" +import { toSlug } from "../lib/slug_converter" const PelayananRoute = new Elysia({ @@ -20,9 +21,21 @@ const PelayananRoute = new Elysia({ }, orderBy: { name: "asc" + }, + select: { + id: true, + name: true, + syaratDokumen: true, + dataPelengkap: true, } }) - return data + + const dataFix = data.map(item => ({ + ...item, + link: `${process.env.BUN_PUBLIC_BASE_URL}/darmasaba/surat?jenis=${toSlug(item.name)}` + })); + + return dataFix }, { detail: { summary: "List Kategori Pelayanan Surat", @@ -299,14 +312,26 @@ const PelayananRoute = new Elysia({ key: string; }[]; - const dataTextFix = dataText.map((item) => { - const nama = dataTextCategory.find((v) => v.key == item.jenis)?.name - return { - id: item.id, - jenis: nama, - value: item.value, - } - }) + const refMap = new Map( + dataTextCategory.map((v, i) => [ + v.key, + { ...v, order: i } + ]) + ); + + const dataTextFix = dataText + .map((item) => { + const ref = refMap.get(item.jenis); + const nama = dataTextCategory.find((v) => v.key == item.jenis)?.name + return { + id: item.id, + jenis: nama, + value: item.value, + order: ref?.order ?? Infinity, + }; + }) + .sort((a, b) => a.order - b.order) + .map(({ order, ...rest }) => rest); // hapus order const dataHistory = await prisma.historyPelayanan.findMany({ where: { @@ -477,7 +502,7 @@ const PelayananRoute = new Elysia({ } }) - return { success: true, message: 'Pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini' } + return { success: true, message: 'Pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini', noPengajuan } }, { body: t.Object({ kategoriId: t.String({ @@ -664,18 +689,29 @@ const PelayananRoute = new Elysia({ 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 { - id: item.id, - key: item.jenis, - value: item.value, - desc: desc ?? '', - name: name ?? '' - } - }) + const refMap = new Map( + dataPelengkapList.map((v, i) => [ + v.key, + { ...v, order: i } + ]) + ); + + const dataTextFix = dataPelengkap + .map((item) => { + const ref = refMap.get(item.jenis); + + return { + id: item.id, + key: item.jenis, + value: item.value, + desc: ref?.desc ?? "", + name: ref?.name ?? "", + order: ref?.order ?? Infinity, + }; + }) + .sort((a, b) => a.order - b.order) + .map(({ order, ...rest }) => rest); // hapus order + const dataHistory = await prisma.historyPelayanan.findMany({ where: { @@ -733,7 +769,6 @@ const PelayananRoute = new Elysia({ dataPelengkap: dataTextFix, } - return datafix }, {