From 8480cec6ae0e02bcbb75a274318bb564bdce4a34 Mon Sep 17 00:00:00 2001 From: amal Date: Tue, 6 Jan 2026 17:00:08 +0800 Subject: [PATCH] upd: notif wa pengajian surat Deskripsi: - upload surat ke seafile - update struktur db - notif wa kirim link download surat - api download surat No Issues; --- prisma/schema.prisma | 1 + src/components/ModalSurat.tsx | 93 ++++++++++--------- .../pelayanan-surat/detail_pelayanan_page.tsx | 28 +++--- src/server/lib/seafile.ts | 19 +++- src/server/routes/pelayanan_surat_route.ts | 2 + src/server/routes/pengaduan_route.ts | 61 +++++++++--- src/server/routes/surat_route.ts | 29 ++++++ 7 files changed, 161 insertions(+), 72 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d64f11e..6f75b42 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -187,6 +187,7 @@ model SuratPelayanan { Warga Warga @relation(fields: [idWarga], references: [id]) idWarga String noSurat String + file String? dateExpired DateTime? @db.Date status Int @default(0) isActive Boolean @default(true) diff --git a/src/components/ModalSurat.tsx b/src/components/ModalSurat.tsx index 2c32271..84284c4 100644 --- a/src/components/ModalSurat.tsx +++ b/src/components/ModalSurat.tsx @@ -23,7 +23,7 @@ export default function ModalSurat({ surat, }: { open: boolean; - onClose: () => void; + onClose: (val: any) => void; surat: string; }) { const A4Style = { @@ -51,69 +51,78 @@ export default function ModalSurat({ const uploadPdf = async () => { try { - setUploading("Mengupload"); - const element = hiddenRef.current; - const canvas = await html2canvas(element, { - scale: 2, - useCORS: true, - allowTaint: true, - width: element.offsetWidth, - height: element.offsetHeight, - }); + if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) { + setUploading("Mengupload"); + 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 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 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; + const imgWidth = pageWidth; + const imgHeight = (canvas.height * pageWidth) / canvas.width; - pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight); + pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight); - // ⬇️ ambil sebagai Blob - const pdfBlob = pdf.output("blob"); + // ⬇️ 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 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", - }); + const resImg = await apiFetch.api.pengaduan.upload.post({ + file: pdfFile, + folder: "surat", + }); + + const resUpdate = await apiFetch.api.surat.update.post({ + id: surat, + filename: resImg.data?.filename!, + }); + + setUploading("Selesai"); + setTimeout(() => { + onClose(resUpdate.data?.link); + }, 1000) + } - console.log(resImg.data) } catch (error) { console.error("Error uploading PDF:", error); - } finally { - setUploading("Selesai"); - setTimeout(() => { - onClose(); - }, 1000) } } useShallowEffect(() => { - setTimeout(() => { - uploadPdf(); - }, 5000); - }, [surat]); + if (open) { + setTimeout(() => { + uploadPdf(); + }, 5000); + } + }, [surat, open]); return ( <> onClose()} + onClose={() => { }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} size="auto" withCloseButton={false} + closeOnClickOutside={false} removeScrollProps={{ allowPinchZoom: true }} styles={{ header: { diff --git a/src/pages/scr/dashboard/pelayanan-surat/detail_pelayanan_page.tsx b/src/pages/scr/dashboard/pelayanan-surat/detail_pelayanan_page.tsx index f62ca0b..767efc6 100644 --- a/src/pages/scr/dashboard/pelayanan-surat/detail_pelayanan_page.tsx +++ b/src/pages/scr/dashboard/pelayanan-surat/detail_pelayanan_page.tsx @@ -101,8 +101,8 @@ function DetailDataPengajuan({ const [openedPreview, setOpenedPreview] = useState(false); const [openedPreviewFile, setOpenedPreviewFile] = useState(false); const [permissions, setPermissions] = useState([]); - const [viewImg, setViewImg] = useState(""); - const [uploading, setUploading] = useState(false) + const [viewImg, setViewImg] = useState({ file: "", folder: "" }); + const [uploading, setUploading] = useState({ ok: false, file: "" }); useEffect(() => { async function fetchHost() { @@ -222,10 +222,10 @@ function DetailDataPengajuan({ }, [viewImg]); useShallowEffect(() => { - if (uploading) { + if (uploading.ok && uploading.file) { sendWA({ status: "selesai", - linkSurat: "", + linkSurat: uploading.file, linkUpdate: "", }); } @@ -235,12 +235,12 @@ function DetailDataPengajuan({ return ( <> { setOpenedPreviewFile(false); }} - folder="syarat-dokumen" - fileName={viewImg} + folder={viewImg.folder} + fileName={viewImg.file} /> {/* MODAL KONFIRMASI */} @@ -312,12 +312,12 @@ function DetailDataPengajuan({ )} - {data?.status == "selesai" && ( + {data?.status == "selesai" && !data?.fileSurat && ( { + onClose={(val) => { setOpenedPreview(false) - setUploading(true) + setUploading({ ok: true, file: val }) }} surat={data?.idSurat} /> @@ -386,7 +386,7 @@ function DetailDataPengajuan({ { - setViewImg(v.value); + setViewImg({ file: v.value, folder: "syarat-dokumen" }); }} > {v.jenis} @@ -473,12 +473,12 @@ function DetailDataPengajuan({ ) : data?.status === "selesai" ? ( - {/* */} + ) : ( <> diff --git a/src/server/lib/seafile.ts b/src/server/lib/seafile.ts index 4343e13..4f14302 100644 --- a/src/server/lib/seafile.ts +++ b/src/server/lib/seafile.ts @@ -248,14 +248,23 @@ export async function moveFile(config: Config, oldName: string, newName: string) return `✏️ Renamed ${oldName} → ${newName}` } -export async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise { - const localName = localFile || remoteFile; - const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`); - const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); +export async function downloadFile(config: Config, fileName: string, folder: string, localFile?: string): Promise { + const localName = localFile || fileName; + // 🔹 gabungkan path folder + file + const filePath = `/${folder}/${fileName}`.replace(/\/+/g, "/"); + // 🔹 encode path agar aman (spasi, dll) + const params = new URLSearchParams({ + p: filePath, + }); + + const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?${params.toString()}`); + if(!downloadUrlResponse.ok) + return 'gagal' + const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer()); await fs.writeFile(localName, buffer); - return `⬇️ Downloaded ${remoteFile} → ${localName}` + return `⬇️ Downloaded ${fileName} → ${localName}` } export async function getFileLink(config: Config, fileName: string): Promise { diff --git a/src/server/routes/pelayanan_surat_route.ts b/src/server/routes/pelayanan_surat_route.ts index f7e491d..ba60a01 100644 --- a/src/server/routes/pelayanan_surat_route.ts +++ b/src/server/routes/pelayanan_surat_route.ts @@ -265,6 +265,7 @@ const PelayananRoute = new Elysia({ select: { id: true, idCategory: true, + file: true } }) @@ -381,6 +382,7 @@ const PelayananRoute = new Elysia({ createdAt: data?.createdAt, updatedAt: data?.updatedAt, idSurat: dataSurat?.id, + fileSurat: dataSurat?.file, } const datafix = { diff --git a/src/server/routes/pengaduan_route.ts b/src/server/routes/pengaduan_route.ts index 8fb9ade..6e1146d 100644 --- a/src/server/routes/pengaduan_route.ts +++ b/src/server/routes/pengaduan_route.ts @@ -1,14 +1,16 @@ -import Elysia, { t } from "elysia" -import type { StatusPengaduan } from "generated/prisma" -import _ from "lodash" -import { v4 as uuidv4 } from "uuid" -import { getLastUpdated } from "../lib/get-last-updated" -import { mimeToExtension } from "../lib/mimetypeToExtension" -import { generateNoPengaduan } from "../lib/no-pengaduan" -import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone" -import { prisma } from "../lib/prisma" -import { renameFile } from "../lib/rename-file" -import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile" +import Elysia, { t } from "elysia"; +import fs from 'fs'; +import type { StatusPengaduan } from "generated/prisma"; +import _ from "lodash"; +import path from "path"; +import { v4 as uuidv4 } from "uuid"; +import { getLastUpdated } from "../lib/get-last-updated"; +import { mimeToExtension } from "../lib/mimetypeToExtension"; +import { generateNoPengaduan } from "../lib/no-pengaduan"; +import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"; +import { prisma } from "../lib/prisma"; +import { renameFile } from "../lib/rename-file"; +import { catFile, defaultConfigSF, downloadFile, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile"; const PengaduanRoute = new Elysia({ prefix: "pengaduan", @@ -605,6 +607,43 @@ const PengaduanRoute = new Elysia({ consumes: ["multipart/form-data"] }, }) + .get("/download", async ({ query, set }) => { + const { file, folder } = query; + + // Validasi file + if (!file) { + return { success: false, message: "File tidak ditemukan" }; + } + + // if (!folder) { + // return { success: false, message: "Folder tidak ditemukan" }; + // } + + const localPath = path.join("/tmp", file); + + // Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer) + // const buffer = await file.arrayBuffer(); + const result = await downloadFile(defaultConfigSF, file, 'surat', localPath); + + if(result=="gagal") { + return { success: false, message: "Download gagal" }; + } + + set.headers["Content-Type"] = "application/pdf"; + set.headers["Content-Disposition"] = `attachment; filename="${file}"`; + + // 🔹 kirim file ke browser + return fs.createReadStream(localPath); + }, { + body: t.Object({ + file: t.Any(), + folder: t.String(), + }), + detail: { + summary: "Download Surat", + description: "Tool untuk download surat dari Seafile", + }, + }) .post("/upload-file-form-data", async ({ body }) => { const { file } = body; diff --git a/src/server/routes/surat_route.ts b/src/server/routes/surat_route.ts index c688522..f5e6f64 100644 --- a/src/server/routes/surat_route.ts +++ b/src/server/routes/surat_route.ts @@ -17,6 +17,7 @@ const SuratRoute = new Elysia({ noSurat: true, idCategory: true, createdAt: true, + file: true, PelayananAjuan: { select: { DataTextPelayanan: true, @@ -44,6 +45,7 @@ const SuratRoute = new Elysia({ idCategory: dataSurat?.idCategory, nameCategory: dataSurat?.CategoryPelayanan?.name, noSurat: dataSurat?.noSurat, + file: dataSurat?.file, dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan, createdAt: dataSurat?.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }), }, @@ -60,6 +62,33 @@ const SuratRoute = new Elysia({ } }) + .post("/update", async ({ body }) => { + const { id, filename } = body + + await prisma.suratPelayanan.update({ + where: { + id, + }, + data: { + file: filename, + } + }) + + return { + success: true, + message: 'surat sudah diperbarui', + link: `${process.env.BUN_PUBLIC_BASE_URL}/api/pengaduan/download?file=${filename}` + } + }, { + body: t.Object({ + id: t.String({ minLength: 1, error: "id harus diisi" }), + filename: t.String({ minLength: 1, error: "filename harus diisi" }), + }), + detail: { + summary: "update file surat", + description: `tool untuk update file surat` + } + }) ; export default SuratRoute