upd: dashbaord admin/

Deksirps:
- format surat
- view file
- api

No Issues
This commit is contained in:
2025-11-21 17:45:12 +08:00
parent 558d8aaafb
commit 41af733c6e
16 changed files with 118 additions and 52 deletions

View File

@@ -16,11 +16,11 @@ import {
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit } from "@tabler/icons-react";
import _ from "lodash";
import { useState } from "react";
import useSWR from "swr";
import ModalFile from "./ModalFile";
import notification from "./notificationGlobal";
import _ from "lodash";
export default function DesaSetting() {
const [btnDisable, setBtnDisable] = useState(false);
@@ -50,7 +50,8 @@ export default function DesaSetting() {
let finalData = { ...dataEdit }; // ← buffer data terbaru
if (dataEdit.name === "TTD") {
const resImg = await apiFetch.api.pengaduan.upload.post({ file: img });
const oldImg = await apiFetch.api.pengaduan["delete-image"].post({ file: dataEdit.value, folder: "lainnya" });
const resImg = await apiFetch.api.pengaduan.upload.post({ file: img, folder: "lainnya" });
if (resImg.status === 200) {
finalData = {
@@ -176,7 +177,7 @@ export default function DesaSetting() {
<ModalFile
open={openedPreview && !_.isEmpty(viewImg)}
onClose={() => setOpenedPreview(false)}
folder="syarat-dokumen"
folder="lainnya"
fileName={viewImg}
/>

View File

@@ -1,10 +1,12 @@
import { detectFileType } from "@/server/lib/detect-type-of-file";
import { Flex, Image, Loader, Modal } from "@mantine/core";
import { useEffect, useState } from "react";
import notification from "./notificationGlobal";
export default function ModalFile({ open, onClose, folder, fileName }: { open: boolean, onClose: () => void, folder: string, fileName: string }) {
const [viewImg, setViewImg] = useState<string>("");
const [viewFile, setViewFile] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [typeFile, setTypeFile] = useState<string>("");
useEffect(() => {
if (open && fileName) {
@@ -12,12 +14,18 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b
}
}, [open, fileName]);
const loadImage = async () => {
try {
setViewImg("");
setViewFile("");
setLoading(true);
// detect type of file
const { ext, type } = detectFileType(fileName);
setTypeFile(type || "");
// load file
const urlApi = '/api/pengaduan/image?folder=' + folder + '&fileName=' + fileName;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
@@ -28,7 +36,7 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
setViewFile(url);
} catch (err) {
console.error("Gagal load gambar:", err);
} finally {
@@ -43,7 +51,7 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b
opened={open}
onClose={onClose}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="lg"
size="xl"
withCloseButton
removeScrollProps={{ allowPinchZoom: true }}
title="File"
@@ -53,13 +61,19 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b
<Loader />
</Flex>
)}
{viewImg && (
<Image
radius="md"
h={300}
fit="contain"
src={viewImg}
/>
{viewFile && (
<>
{typeFile == "pdf" ? (
<embed src={viewFile} type="application/pdf" width="100%" height="100%" />
) : (
<Image
radius="md"
h={300}
fit="contain"
src={viewFile}
/>
)}
</>
)}
</Modal>
);

View File

@@ -14,7 +14,7 @@ export default function SKBedaBiodataDiri({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -14,7 +14,7 @@ export default function SKBelumKawin({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -14,7 +14,7 @@ export default function SKDomisiliOrganisasi({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -14,7 +14,7 @@ export default function SKKelahiran({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -14,7 +14,7 @@ export default function SKKelakuanBaik({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -14,7 +14,7 @@ export default function SKKematian({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -14,7 +14,7 @@ export default function SKPenghasilan({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -12,7 +12,7 @@ export default function SKTempatUsaha({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -13,7 +13,7 @@ export default function SKTidakMampu({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -14,7 +14,7 @@ export default function SKUsaha({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -13,7 +13,7 @@ export default function SKYatim({ data }: { data: any }) {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)

View File

@@ -0,0 +1,25 @@
function getExtension(fileName: string): string | null {
if (!fileName || typeof fileName !== "string") return null;
const parts = fileName.split(".");
if (parts.length <= 1) return null;
return parts.pop()?.toLowerCase() || null;
}
export function detectFileType(fileName: string) {
const ext = getExtension(fileName);
if (!ext) return { ext: null, type: "unknown" };
if (["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(ext)) {
return { ext, type: "image" };
}
if (ext === "pdf") {
return { ext, type: "pdf" };
}
return { ext, type: "other" };
}

View File

@@ -94,7 +94,6 @@ export async function fetchWithAuth(config: Config, url: string, options: Reques
} catch {
console.error('🔍 Could not read response body');
}
process.exit(1);
}
return response;
}
@@ -139,7 +138,7 @@ export async function catFile(config: Config, folder: string, fileName: string):
return buffer;
}
export async function uploadFile(config: Config, file: File): Promise<string> {
export async function uploadFile(config: Config, file: File, folder: string): Promise<string> {
const remoteName = path.basename(file.name);
// 1. Dapatkan upload link (pakai Authorization)
@@ -152,7 +151,7 @@ export async function uploadFile(config: Config, file: File): Promise<string> {
// 2. Siapkan form-data
const formData = new FormData();
formData.append("parent_dir", "/");
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
formData.append("relative_path", folder); // tanpa slash di akhir
formData.append("file", file, remoteName); // file langsung, jangan pakai Blob
// 3. Upload file TANPA Authorization header, token di query param
@@ -232,10 +231,10 @@ export async function uploadFileToFolder(config: Config, base64File: { name: str
}
export async function removeFile(config: Config, fileName: string, folder: string): Promise<string> {
const res = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${folder}/${fileName}`, { method: 'DELETE' });
export async function removeFile(config: Config, fileName: string): Promise<string> {
await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`, { method: 'DELETE' });
if (!res.ok) return 'gagal menghapus file';
return `🗑️ Removed ${fileName}`
}

View File

@@ -7,7 +7,7 @@ import { generateNoPengaduan } from "../lib/no-pengaduan"
import { normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
import { renameFile } from "../lib/rename-file"
import { catFile, defaultConfigSF, uploadFile, uploadFileBase64 } from "../lib/seafile"
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileBase64 } from "../lib/seafile"
const PengaduanRoute = new Elysia({
prefix: "pengaduan",
@@ -176,7 +176,7 @@ const PengaduanRoute = new Elysia({
title: judulPengaduan,
detail: detailPengaduan,
idCategory: idCategoryFix,
idWarga: idWargaFix,
idWarga: idWargaFix || "",
location: lokasi,
image: imageFix,
noPengaduan,
@@ -218,23 +218,20 @@ const PengaduanRoute = new Elysia({
description: "Alamat atau titik lokasi pengaduan"
}),
namaGambar: t.String({
optional: true,
namaGambar: t.Optional(t.String({
examples: ["sampah.jpg"],
description: "Nama file gambar yang telah diupload (opsional)"
}),
})),
kategoriId: t.String({
optional: true,
kategoriId: t.Optional(t.String({
examples: ["kebersihan"],
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
}),
})),
wargaId: t.String({
optional: true,
wargaId: t.Optional(t.String({
examples: ["budiman"],
description: "ID unik warga yang melapor (jika sudah terdaftar)"
}),
})),
noTelepon: t.String({
error: "Nomor telepon harus diisi",
@@ -517,7 +514,7 @@ Respon:
}
})
.post("/upload", async ({ body }) => {
const { file } = body;
const { file, folder } = body;
// Validasi file
if (!file) {
@@ -530,7 +527,7 @@ Respon:
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
// const buffer = await file.arrayBuffer();
const result = await uploadFile(defaultConfigSF, renamedFile);
const result = await uploadFile(defaultConfigSF, renamedFile, folder);
if (result == 'gagal') {
return { success: false, message: "Upload gagal" };
}
@@ -544,7 +541,8 @@ Respon:
};
}, {
body: t.Object({
file: t.Any()
file: t.Any(),
folder: t.String(),
}),
detail: {
summary: "Upload File",
@@ -728,12 +726,14 @@ Respon:
const hasil = await catFile(defaultConfigSF, folder, fileName);
const ext = fileName.split(".").pop()?.toLowerCase();
const mime =
ext === "jpg" || ext === "jpeg"
? "image/jpeg"
: ext === "png"
? "image/png"
: "application/octet-stream";
let mime = "application/octet-stream"; // default
if (["jpg", "jpeg"].includes(ext!)) mime = "image/jpeg";
if (["png"].includes(ext!)) mime = "image/png";
if (["gif"].includes(ext!)) mime = "image/gif";
if (["webp"].includes(ext!)) mime = "image/webp";
if (["svg"].includes(ext!)) mime = "image/svg+xml";
if (["pdf"].includes(ext!)) mime = "application/pdf";
set.headers["Content-Type"] = mime;
set.headers["Content-Length"] = hasil.byteLength.toString();
@@ -749,6 +749,33 @@ Respon:
description: "tool untuk mendapatkan gambar",
}
})
.post("/delete-image", async ({ body }) => {
const { file, folder } = body;
// Validasi file
if (!file) {
return { success: false, message: "File tidak ditemukan" };
}
const result = await removeFile(defaultConfigSF, file, folder);
if (result == 'gagal') {
return { success: false, message: "Delete gagal" };
}
return {
success: true,
message: "Delete berhasil",
};
}, {
body: t.Object({
file: t.String(),
folder: t.String(),
}),
detail: {
summary: "Delete File",
description: "Tool untuk delete file Seafile",
},
})
;