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:
@@ -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)
|
||||||
|
|||||||
@@ -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,69 +51,78 @@ export default function ModalSurat({
|
|||||||
|
|
||||||
const uploadPdf = async () => {
|
const uploadPdf = async () => {
|
||||||
try {
|
try {
|
||||||
setUploading("Mengupload");
|
if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) {
|
||||||
const element = hiddenRef.current;
|
setUploading("Mengupload");
|
||||||
const canvas = await html2canvas(element, {
|
const element = hiddenRef.current;
|
||||||
scale: 2,
|
const canvas = await html2canvas(element, {
|
||||||
useCORS: true,
|
scale: 2,
|
||||||
allowTaint: true,
|
useCORS: true,
|
||||||
width: element.offsetWidth,
|
allowTaint: true,
|
||||||
height: element.offsetHeight,
|
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 pdf = new jsPDF("p", "mm", "a4");
|
||||||
const pageWidth = 210; // A4 width mm
|
const pageWidth = 210; // A4 width mm
|
||||||
const pageHeight = 297; // A4 height mm
|
const pageHeight = 297; // A4 height mm
|
||||||
|
|
||||||
const imgWidth = pageWidth;
|
const imgWidth = pageWidth;
|
||||||
const imgHeight = (canvas.height * pageWidth) / canvas.width;
|
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
|
// ⬇️ ambil sebagai Blob
|
||||||
const pdfBlob = pdf.output("blob");
|
const pdfBlob = pdf.output("blob");
|
||||||
|
|
||||||
const pdfFile = new File(
|
const pdfFile = new File(
|
||||||
[pdfBlob],
|
[pdfBlob],
|
||||||
`${data?.data?.surat?.nameCategory}.pdf`,
|
`${data?.data?.surat?.nameCategory}.pdf`,
|
||||||
{
|
{
|
||||||
type: "application/pdf",
|
type: "application/pdf",
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const resImg = await apiFetch.api.pengaduan.upload.post({
|
const resImg = await apiFetch.api.pengaduan.upload.post({
|
||||||
file: pdfFile,
|
file: pdfFile,
|
||||||
folder: "surat",
|
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) {
|
} catch (error) {
|
||||||
console.error("Error uploading PDF:", error);
|
console.error("Error uploading PDF:", error);
|
||||||
} finally {
|
|
||||||
setUploading("Selesai");
|
|
||||||
setTimeout(() => {
|
|
||||||
onClose();
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
setTimeout(() => {
|
if (open) {
|
||||||
uploadPdf();
|
setTimeout(() => {
|
||||||
}, 5000);
|
uploadPdf();
|
||||||
}, [surat]);
|
}, 5000);
|
||||||
|
}
|
||||||
|
}, [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: {
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -430,8 +432,8 @@ export default function FormSurat() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const formatted = e
|
const formatted = e
|
||||||
? dayjs(e)
|
? dayjs(e)
|
||||||
.locale("id")
|
.locale("id")
|
||||||
.format("DD MMMM YYYY")
|
.format("DD MMMM YYYY")
|
||||||
: "";
|
: "";
|
||||||
validationForm({
|
validationForm({
|
||||||
key: "dataPelengkap",
|
key: "dataPelengkap",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user