Merge pull request 'amalia/06-jan-26' (#101) from amalia/06-jan-26 into main

Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/101
This commit is contained in:
2026-01-06 17:45:44 +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]) Warga Warga @relation(fields: [idWarga], references: [id])
idWarga String idWarga String
noSurat String noSurat String
file String?
dateExpired DateTime? @db.Date dateExpired DateTime? @db.Date
status Int @default(0) status Int @default(0)
isActive Boolean @default(true) isActive Boolean @default(true)

View File

@@ -23,7 +23,7 @@ export default function ModalSurat({
surat, surat,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: (val: any) => void;
surat: string; surat: string;
}) { }) {
const A4Style = { const A4Style = {
@@ -51,6 +51,7 @@ export default function ModalSurat({
const uploadPdf = async () => { const uploadPdf = async () => {
try { try {
if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) {
setUploading("Mengupload"); setUploading("Mengupload");
const element = hiddenRef.current; const element = hiddenRef.current;
const canvas = await html2canvas(element, { const canvas = await html2canvas(element, {
@@ -89,31 +90,39 @@ export default function ModalSurat({
folder: "surat", folder: "surat",
}); });
console.log(resImg.data) const resUpdate = await apiFetch.api.surat.update.post({
} catch (error) { id: surat,
console.error("Error uploading PDF:", error); filename: resImg.data?.filename!,
} finally { });
setUploading("Selesai"); setUploading("Selesai");
setTimeout(() => { setTimeout(() => {
onClose(); onClose(resUpdate.data?.link);
}, 1000) }, 1000)
} }
} catch (error) {
console.error("Error uploading PDF:", error);
}
} }
useShallowEffect(() => { useShallowEffect(() => {
if (open) {
setTimeout(() => { setTimeout(() => {
uploadPdf(); uploadPdf();
}, 5000); }, 5000);
}, [surat]); }
}, [surat, open]);
return ( return (
<> <>
<Modal <Modal
opened={open} opened={open}
onClose={() => onClose()} onClose={() => { }}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto" size="auto"
withCloseButton={false} withCloseButton={false}
closeOnClickOutside={false}
removeScrollProps={{ allowPinchZoom: true }} removeScrollProps={{ allowPinchZoom: true }}
styles={{ styles={{
header: { header: {

View File

@@ -324,6 +324,7 @@ export default function FormSurat() {
<Grid> <Grid>
<Grid.Col span={12}> <Grid.Col span={12}>
<Select <Select
allowDeselect={false}
label={ label={
<FieldLabel <FieldLabel
label="Jenis Surat" label="Jenis Surat"
@@ -396,6 +397,7 @@ export default function FormSurat() {
<Grid.Col span={6} key={index}> <Grid.Col span={6} key={index}>
{item.type == "enum" ? ( {item.type == "enum" ? (
<Select <Select
allowDeselect={false}
label={ label={
<FieldLabel <FieldLabel
label={item.name} label={item.name}

View File

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

View File

@@ -101,8 +101,8 @@ function DetailDataPengajuan({
const [openedPreview, setOpenedPreview] = useState(false); const [openedPreview, setOpenedPreview] = useState(false);
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({ file: "", folder: "" });
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState({ ok: false, file: "" });
useEffect(() => { useEffect(() => {
async function fetchHost() { async function fetchHost() {
@@ -222,10 +222,10 @@ function DetailDataPengajuan({
}, [viewImg]); }, [viewImg]);
useShallowEffect(() => { useShallowEffect(() => {
if (uploading) { if (uploading.ok && uploading.file) {
sendWA({ sendWA({
status: "selesai", status: "selesai",
linkSurat: "", linkSurat: uploading.file,
linkUpdate: "", linkUpdate: "",
}); });
} }
@@ -235,12 +235,12 @@ function DetailDataPengajuan({
return ( return (
<> <>
<ModalFile <ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)} open={openedPreviewFile && !_.isEmpty(viewImg.file)}
onClose={() => { onClose={() => {
setOpenedPreviewFile(false); setOpenedPreviewFile(false);
}} }}
folder="syarat-dokumen" folder={viewImg.folder}
fileName={viewImg} fileName={viewImg.file}
/> />
{/* MODAL KONFIRMASI */} {/* MODAL KONFIRMASI */}
@@ -312,12 +312,12 @@ function DetailDataPengajuan({
)} )}
</Stack> </Stack>
</Modal> </Modal>
{data?.status == "selesai" && ( {data?.status == "selesai" && !data?.fileSurat && (
<ModalSurat <ModalSurat
open={openedPreview} open={openedPreview}
onClose={() => { onClose={(val) => {
setOpenedPreview(false) setOpenedPreview(false)
setUploading(true) setUploading({ ok: true, file: val })
}} }}
surat={data?.idSurat} surat={data?.idSurat}
/> />
@@ -386,7 +386,7 @@ function DetailDataPengajuan({
<List.Item key={v.id}> <List.Item key={v.id}>
<Anchor <Anchor
onClick={() => { onClick={() => {
setViewImg(v.value); setViewImg({ file: v.value, folder: "syarat-dokumen" });
}} }}
> >
{v.jenis} {v.jenis}
@@ -473,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={() => { setViewImg({ file: data?.fileSurat, folder: "surat" }) }}
> >
Surat Surat
</Button> */} </Button>
</Group> </Group>
) : ( ) : (
<></> <></>

View File

@@ -248,14 +248,23 @@ export async function moveFile(config: Config, oldName: string, newName: string)
return `✏️ Renamed ${oldName}${newName}` return `✏️ Renamed ${oldName}${newName}`
} }
export async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise<string> { export async function downloadFile(config: Config, fileName: string, folder: string, localFile?: string): Promise<string> {
const localName = localFile || remoteFile; const localName = localFile || fileName;
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`); // 🔹 gabungkan path folder + file
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); 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()); const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer());
await fs.writeFile(localName, buffer); await fs.writeFile(localName, buffer);
return `⬇️ Downloaded ${remoteFile}${localName}` return `⬇️ Downloaded ${fileName}${localName}`
} }
export async function getFileLink(config: Config, fileName: string): Promise<string> { export async function getFileLink(config: Config, fileName: string): Promise<string> {

View File

@@ -265,6 +265,7 @@ const PelayananRoute = new Elysia({
select: { select: {
id: true, id: true,
idCategory: true, idCategory: true,
file: true
} }
}) })
@@ -381,6 +382,7 @@ const PelayananRoute = new Elysia({
createdAt: data?.createdAt, createdAt: data?.createdAt,
updatedAt: data?.updatedAt, updatedAt: data?.updatedAt,
idSurat: dataSurat?.id, idSurat: dataSurat?.id,
fileSurat: dataSurat?.file,
} }
const datafix = { const datafix = {

View File

@@ -1,14 +1,16 @@
import Elysia, { t } from "elysia" import Elysia, { t } from "elysia";
import type { StatusPengaduan } from "generated/prisma" import fs from 'fs';
import _ from "lodash" import type { StatusPengaduan } from "generated/prisma";
import { v4 as uuidv4 } from "uuid" import _ from "lodash";
import { getLastUpdated } from "../lib/get-last-updated" import path from "path";
import { mimeToExtension } from "../lib/mimetypeToExtension" import { v4 as uuidv4 } from "uuid";
import { generateNoPengaduan } from "../lib/no-pengaduan" import { getLastUpdated } from "../lib/get-last-updated";
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone" import { mimeToExtension } from "../lib/mimetypeToExtension";
import { prisma } from "../lib/prisma" import { generateNoPengaduan } from "../lib/no-pengaduan";
import { renameFile } from "../lib/rename-file" import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone";
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile" 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({ const PengaduanRoute = new Elysia({
prefix: "pengaduan", prefix: "pengaduan",
@@ -605,6 +607,43 @@ const PengaduanRoute = new Elysia({
consumes: ["multipart/form-data"] 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 }) => { .post("/upload-file-form-data", async ({ body }) => {
const { file } = body; const { file } = body;

View File

@@ -17,6 +17,7 @@ const SuratRoute = new Elysia({
noSurat: true, noSurat: true,
idCategory: true, idCategory: true,
createdAt: true, createdAt: true,
file: true,
PelayananAjuan: { PelayananAjuan: {
select: { select: {
DataTextPelayanan: true, DataTextPelayanan: true,
@@ -44,6 +45,7 @@ const SuratRoute = new Elysia({
idCategory: dataSurat?.idCategory, idCategory: dataSurat?.idCategory,
nameCategory: dataSurat?.CategoryPelayanan?.name, nameCategory: dataSurat?.CategoryPelayanan?.name,
noSurat: dataSurat?.noSurat, noSurat: dataSurat?.noSurat,
file: dataSurat?.file,
dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan, dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan,
createdAt: dataSurat?.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }), 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 export default SuratRoute