Compare commits

...

3 Commits

Author SHA1 Message Date
476319945e upd: validasi form create dan update pengajuan surat
Deskripsi:
- deselect false pada input select form tambah dan update pengajuan surat

No Issues
2026-01-06 17:44:16 +08:00
8480cec6ae upd: notif wa pengajian surat
Deskripsi:
- upload surat ke seafile
- update struktur db
- notif wa kirim link download surat
- api download surat

No Issues;
2026-01-06 17:00:08 +08:00
4ca5e4c4f3 Merge pull request 'upd: pengajuan surat' (#100) from amalia/05-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/100
2026-01-05 17:25:27 +08:00
9 changed files with 166 additions and 74 deletions

View File

@@ -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)

View File

@@ -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 (
<>
<Modal
opened={open}
onClose={() => onClose()}
onClose={() => { }}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto"
withCloseButton={false}
closeOnClickOutside={false}
removeScrollProps={{ allowPinchZoom: true }}
styles={{
header: {

View File

@@ -324,6 +324,7 @@ export default function FormSurat() {
<Grid>
<Grid.Col span={12}>
<Select
allowDeselect={false}
label={
<FieldLabel
label="Jenis Surat"
@@ -396,6 +397,7 @@ export default function FormSurat() {
<Grid.Col span={6} key={index}>
{item.type == "enum" ? (
<Select
allowDeselect={false}
label={
<FieldLabel
label={item.name}
@@ -430,8 +432,8 @@ export default function FormSurat() {
onChange={(e) => {
const formatted = e
? dayjs(e)
.locale("id")
.format("DD MMMM YYYY")
.locale("id")
.format("DD MMMM YYYY")
: "";
validationForm({
key: "dataPelengkap",

View File

@@ -625,6 +625,7 @@ function DataUpdate({
<Grid.Col span={6} key={index}>
{item.type == "enum" ? (
<Select
allowDeselect={false}
label={<FieldLabel label={item.name} hint={item.desc} />}
data={item.options ?? []}
placeholder={item.name}

View File

@@ -101,8 +101,8 @@ function DetailDataPengajuan({
const [openedPreview, setOpenedPreview] = useState(false);
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
const [permissions, setPermissions] = useState<JsonValue[]>([]);
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 (
<>
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)}
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
onClose={() => {
setOpenedPreviewFile(false);
}}
folder="syarat-dokumen"
fileName={viewImg}
folder={viewImg.folder}
fileName={viewImg.file}
/>
{/* MODAL KONFIRMASI */}
@@ -312,12 +312,12 @@ function DetailDataPengajuan({
)}
</Stack>
</Modal>
{data?.status == "selesai" && (
{data?.status == "selesai" && !data?.fileSurat && (
<ModalSurat
open={openedPreview}
onClose={() => {
onClose={(val) => {
setOpenedPreview(false)
setUploading(true)
setUploading({ ok: true, file: val })
}}
surat={data?.idSurat}
/>
@@ -386,7 +386,7 @@ function DetailDataPengajuan({
<List.Item key={v.id}>
<Anchor
onClick={() => {
setViewImg(v.value);
setViewImg({ file: v.value, folder: "syarat-dokumen" });
}}
>
{v.jenis}
@@ -473,12 +473,12 @@ function DetailDataPengajuan({
</Group>
) : data?.status === "selesai" ? (
<Group justify="center" grow>
{/* <Button
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
onClick={() => { setViewImg({ file: data?.fileSurat, folder: "surat" }) }}
>
Surat
</Button> */}
</Button>
</Group>
) : (
<></>

View File

@@ -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<string> {
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<string> {
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<string> {

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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