upd: pengajuan surat

Deskripsi:
- send wa penolakan + lik update
- send wa diterima
- upload ke seafile
- blm selesai ngirim link surat ke wa

No Issues
This commit is contained in:
2026-01-05 17:24:53 +08:00
parent 2d336ea467
commit 75758bcbe6
6 changed files with 380 additions and 44 deletions

146
bak/ModalSurat.tsx.txt Normal file
View File

@@ -0,0 +1,146 @@
import apiFetch from "@/lib/apiFetch";
import { ActionIcon, Flex, Modal } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconDownload, IconX } from "@tabler/icons-react";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
import { useRef } from "react";
import useSWR from "swr";
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin";
import SKDomisiliOrganisasi from "./surat/SKDomisiliOrganisasi";
import SKKelahiran from "./surat/SKKelahiran";
import SKKelakuanBaik from "./surat/SKKelakuanBaik";
import SKKematian from "./surat/SKKematian";
import SKPenghasilan from "./surat/SKPenghasilan";
import SKTempatUsaha from "./surat/SKTempatUsaha";
import SKTidakMampu from "./surat/SKTidakMampu";
import SKUsaha from "./surat/SKUsaha";
import SKYatim from "./surat/SKYatimPiatu";
export default function ModalSurat({
open,
onClose,
surat,
}: {
open: boolean;
onClose: () => void;
surat: string;
}) {
const A4Style = {
width: "210mm",
height: "297mm",
padding: "20mm",
background: "#fff",
color: "#000",
fontSize: "14px",
fontFamily: "Times New Roman",
};
const hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({
query: {
id: surat,
},
}),
);
useShallowEffect(() => {
mutate();
}, []);
const downloadPDF = async () => {
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width;
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
pdf.save(`${data?.data?.surat?.nameCategory}.pdf`);
};
return (
<>
<Modal
opened={open}
onClose={() => onClose()}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto"
withCloseButton={false}
removeScrollProps={{ allowPinchZoom: true }}
styles={{
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px 16px",
},
title: {
width: "100%",
},
}}
title={
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
<Flex gap={8}>
<ActionIcon size={32} variant="default">
<IconDownload size={20} onClick={downloadPDF} />
</ActionIcon>
<ActionIcon size={32} variant="default" onClick={onClose}>
<IconX size={20} />
</ActionIcon>
</Flex>
</Flex>
}
>
<div ref={hiddenRef} style={A4Style}>
{data && data.data ? (
data.data.surat.idCategory == "skusaha" ? (
<SKUsaha data={data.data} />
) : data.data.surat.idCategory == "skkelahiran" ? (
<SKKelahiran data={data.data} />
) : data.data.surat.idCategory == "skkelakuanbaik" ? (
<SKKelakuanBaik data={data.data} />
) : data.data.surat.idCategory == "skpenghasilan" ? (
<SKPenghasilan data={data.data} />
) : data.data.surat.idCategory == "sktidakmampu" ? (
<SKTidakMampu data={data.data} />
) : data.data.surat.idCategory == "skyatimpiatu" ? (
<SKYatim data={data.data} />
) : data.data.surat.idCategory == "skdomisiliorganisasi" ? (
<SKDomisiliOrganisasi data={data.data} />
) : data.data.surat.idCategory == "skbedabiodata" ? (
<SKBedaBiodataDiri data={data.data} />
) : data.data.surat.idCategory == "sktempatusaha" ? (
<SKTempatUsaha data={data.data} />
) : data.data.surat.idCategory == "skbelumkawin" ? (
<SKBelumKawin data={data.data} />
) : data.data.surat.idCategory == "skkematian" ? (
<SKKematian data={data.data} />
) : (
<></>
)
) : (
<></>
)}
</div>
</Modal>
</>
);
}

View File

@@ -1,10 +1,9 @@
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { ActionIcon, Flex, Modal } from "@mantine/core"; import { Flex, Loader, Modal, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { IconDownload, IconX } from "@tabler/icons-react";
import html2canvas from "html2canvas"; import html2canvas from "html2canvas";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import { useRef } from "react"; import { useRef, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri"; import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin"; import SKBelumKawin from "./surat/SKBelumKawin";
@@ -36,6 +35,7 @@ export default function ModalSurat({
fontSize: "14px", fontSize: "14px",
fontFamily: "Times New Roman", fontFamily: "Times New Roman",
}; };
const [uploading, setUploading] = useState<"Menyiapkan" | "Mengupload" | "Selesai">("Menyiapkan")
const hiddenRef = useRef<any>(null); const hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () => const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({ apiFetch.api.surat.detail.get({
@@ -49,7 +49,9 @@ export default function ModalSurat({
mutate(); mutate();
}, []); }, []);
const downloadPDF = async () => { const uploadPdf = async () => {
try {
setUploading("Mengupload");
const element = hiddenRef.current; const element = hiddenRef.current;
const canvas = await html2canvas(element, { const canvas = await html2canvas(element, {
scale: 2, scale: 2,
@@ -70,8 +72,39 @@ export default function ModalSurat({
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight); pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
pdf.save(`${data?.data?.surat?.nameCategory}.pdf`); // ⬇️ ambil sebagai Blob
}; const pdfBlob = pdf.output("blob");
const pdfFile = new File(
[pdfBlob],
`${data?.data?.surat?.nameCategory}.pdf`,
{
type: "application/pdf",
lastModified: Date.now(),
}
);
const resImg = await apiFetch.api.pengaduan.upload.post({
file: pdfFile,
folder: "surat",
});
console.log(resImg.data)
} catch (error) {
console.error("Error uploading PDF:", error);
} finally {
setUploading("Selesai");
setTimeout(() => {
onClose();
}, 1000)
}
}
useShallowEffect(() => {
setTimeout(() => {
uploadPdf();
}, 5000);
}, [surat]);
return ( return (
<> <>
@@ -97,14 +130,9 @@ export default function ModalSurat({
<Flex justify="space-between" align="center" w="100%"> <Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div> <div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
<Flex gap={8}> <Flex gap={8} align="center">
<ActionIcon size={32} variant="default"> <Loader color="blue" size="xs" />
<IconDownload size={20} onClick={downloadPDF} /> <Text size="sm">{uploading}</Text>
</ActionIcon>
<ActionIcon size={32} variant="default" onClick={onClose}>
<IconX size={20} />
</ActionIcon>
</Flex> </Flex>
</Flex> </Flex>
} }

View File

@@ -62,6 +62,7 @@ export default function DetailPengajuanPage() {
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataPengajuan <DetailDataPengajuan
data={data?.data?.pengajuan} data={data?.data?.pengajuan}
warga={data && data.data && data.data.warga ? data.data.warga : undefined}
syaratDokumen={data?.data?.syaratDokumen} syaratDokumen={data?.data?.syaratDokumen}
dataText={data?.data?.dataText} dataText={data?.data?.dataText}
onAction={() => { onAction={() => {
@@ -81,11 +82,13 @@ export default function DetailPengajuanPage() {
function DetailDataPengajuan({ function DetailDataPengajuan({
data, data,
warga,
syaratDokumen, syaratDokumen,
dataText, dataText,
onAction, onAction,
}: { }: {
data: any; data: any;
warga?: { phone?: string | null } | null;
syaratDokumen: any; syaratDokumen: any;
dataText: any; dataText: any;
onAction: () => void; onAction: () => void;
@@ -99,6 +102,7 @@ function DetailDataPengajuan({
const [openedPreviewFile, setOpenedPreviewFile] = useState(false); const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
const [permissions, setPermissions] = useState<JsonValue[]>([]); const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [viewImg, setViewImg] = useState(""); const [viewImg, setViewImg] = useState("");
const [uploading, setUploading] = useState(false)
useEffect(() => { useEffect(() => {
async function fetchHost() { async function fetchHost() {
@@ -115,22 +119,78 @@ function DetailDataPengajuan({
fetchHost(); fetchHost();
}, []); }, []);
async function sendWA({ status, linkSurat, linkUpdate }: { status: string, linkSurat: string, linkUpdate: string }) {
try {
const resWA = await apiFetch.api["send-wa"]["pengajuan-surat"].post({
noPengajuan: data?.noPengajuan ?? "",
jenisSurat: data?.category ?? "",
alasan: keterangan,
status,
linkSurat,
linkUpdate,
tlp: warga?.phone ?? "",
})
if (resWA?.status === 200) {
if (resWA.data?.success) {
notification({
title: "Success",
message: "Success send message to warga",
type: "success",
});
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} catch (error) {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
}
const handleKonfirmasi = async (cat: "terima" | "tolak") => { const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try { try {
const res = await apiFetch.api.pelayanan["update-status"].post({ const statusFix = cat == "tolak"
id: data?.id,
status:
cat == "tolak"
? "ditolak" ? "ditolak"
: data.status == "antrian" : data.status == "antrian"
? "diterima" ? "diterima"
: "selesai", : "selesai"
const res = await apiFetch.api.pelayanan["update-status"].post({
id: data?.id,
status: statusFix,
keterangan: keterangan, keterangan: keterangan,
idUser: host?.id ?? "", idUser: host?.id ?? "",
noSurat: noSurat, noSurat: noSurat,
}); });
if (res?.status === 200) { if (res?.status === 200) {
if (statusFix == "selesai") {
setTimeout(() => {
setOpenedPreview(true)
}, 1000)
} else {
sendWA({
status: statusFix,
linkSurat: "",
linkUpdate: statusFix == "ditolak" ? res.data?.linkUpdate ?? '' : '',
});
}
onAction(); onAction();
close(); close();
notification({ notification({
@@ -161,6 +221,17 @@ function DetailDataPengajuan({
} }
}, [viewImg]); }, [viewImg]);
useShallowEffect(() => {
if (uploading) {
sendWA({
status: "selesai",
linkSurat: "",
linkUpdate: "",
});
}
}, [uploading]);
return ( return (
<> <>
<ModalFile <ModalFile
@@ -244,7 +315,10 @@ function DetailDataPengajuan({
{data?.status == "selesai" && ( {data?.status == "selesai" && (
<ModalSurat <ModalSurat
open={openedPreview} open={openedPreview}
onClose={() => setOpenedPreview(false)} onClose={() => {
setOpenedPreview(false)
setUploading(true)
}}
surat={data?.idSurat} surat={data?.idSurat}
/> />
)} )}
@@ -399,12 +473,12 @@ function DetailDataPengajuan({
</Group> </Group>
) : data?.status === "selesai" ? ( ) : data?.status === "selesai" ? (
<Group justify="center" grow> <Group justify="center" grow>
<Button {/* <Button
variant="light" variant="light"
onClick={() => setOpenedPreview(!openedPreview)} onClick={() => setOpenedPreview(!openedPreview)}
> >
Surat Surat
</Button> </Button> */}
</Group> </Group>
) : ( ) : (
<></> <></>

View File

@@ -8,16 +8,19 @@ export async function createSurat({ idPengajuan, idCategory, idWarga, noSurat }:
idCategory, idCategory,
idWarga, idWarga,
noSurat, noSurat,
},
select: {
id: true
} }
}) })
if (!surat.id) { if (!surat.id) {
return { success: false, message: 'gagal membuat surat' } return { success: false, message: 'gagal membuat surat', idSurat: '' }
} }
return { success: true, message: 'surat sudah dibuat' } return { success: true, message: 'surat sudah dibuat', idSurat: surat.id }
} catch (error) { } catch (error) {
console.log(error) console.error(error)
return { success: false, message: 'gagal membuat surat' } return { success: false, message: 'gagal membuat surat' }
} }

View File

@@ -834,9 +834,14 @@ const PelayananRoute = new Elysia({
}) })
if (!pengajuan) { if (!pengajuan) {
return { success: false, message: 'gagal update status pengajuan surat' } return { success: false, message: 'gagal update status pengajuan surat', linkUpdate: '', idSurat: '' }
} }
const dataPengajuan = await prisma.pelayananAjuan.findUnique({
where: { id: pengajuan.id },
select: { noPengajuan: true }
});
if (status === "diterima") { if (status === "diterima") {
deskripsi = "Pengajuan surat diterima" deskripsi = "Pengajuan surat diterima"
} else if (status === "ditolak") { } else if (status === "ditolak") {
@@ -855,11 +860,19 @@ const PelayananRoute = new Elysia({
} }
}) })
let idSurat = "";
if (status === "selesai") { if (status === "selesai") {
await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat }) const result = await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat })
idSurat = result.idSurat ?? "";
} }
return { success: true, message: 'pengajuan surat sudah diperbarui' } return {
success: true,
message: 'pengajuan surat sudah diperbarui',
linkUpdate: status == "ditolak" ? `${process.env.BUN_PUBLIC_BASE_URL}/darmasaba/update-data-surat?pengajuan=${dataPengajuan?.noPengajuan}` : '',
idSurat: idSurat,
}
}, { }, {
body: t.Object({ body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }), id: t.String({ minLength: 1, error: "id harus diisi" }),

View File

@@ -76,6 +76,78 @@ Terima kasih atas partisipasi dan kepercayaan Bapak/Ibu.`
description: `tool untuk send pemberitahuan pengaduan lewat WA` description: `tool untuk send pemberitahuan pengaduan lewat WA`
} }
}) })
.post("/pengajuan-surat", async ({ body }) => {
const { noPengajuan, jenisSurat, status, alasan, tlp, linkSurat, linkUpdate } = body
let text = ""
if (status === "ditolak") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat tersebut tidak dapat diproses (ditolak).
Alasan penolakan: ${alasan}
Bapak/Ibu dapat melakukan perbaikan atau pembaruan data melalui tautan berikut:
👉 ${linkUpdate}
Setelah data diperbarui, pengajuan akan diproses kembali sesuai ketentuan yang berlaku.
Terima kasih atas pengertian Bapak/Ibu.`
} else if (status == "diterima") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat yang Bapak/Ibu ajukan telah kami terima dan sedang menunggu proses verifikasi serta penanganan lebih lanjut.
Terima kasih atas kesabaran Bapak/Ibu.`
} else if (status == "selesai") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat tersebut telah selesai diproses.
Bapak/Ibu dapat mengunduh surat melalui tautan berikut:
👉 ${linkSurat}
Terima kasih atas kepercayaan Bapak/Ibu.`
}
const textFix = encodeURIComponent(text)
const res = await fetch(
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${tlp}&text=${textFix}`,
{
cache: "no-cache",
headers: {
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
},
}
);
if (res.status !== 200)
return { success: false, message: "Nomor Whatsapp Tidak Aktif" }
return { success: true, message: 'Pemberitahuan berhasil dikirim ke warga' }
}, {
body: t.Object({
noPengajuan: t.String({ minLength: 1, error: "nomer pengajuan harus diisi" }),
jenisSurat: t.String({ minLength: 1, error: "jenis surat harus diisi" }),
status: t.String({ minLength: 1, error: "status harus diisi" }),
alasan: t.String({ optional: true }),
linkSurat: t.String({ optional: true }),
linkUpdate: t.String({ optional: true }),
tlp: t.String({ minLength: 1, error: "nomor telepon harus diisi" }),
}),
detail: {
summary: "Send pemberitahuan pengajuan surat lewat WA",
description: `tool untuk send pemberitahuan pengajuan surat lewat WA`
}
})
; ;
export default SendWaRoute export default SendWaRoute