Compare commits
19 Commits
amalia/07-
...
2aaa44cf14
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aaa44cf14 | |||
| fbf00a55da | |||
|
|
03955743ca | ||
|
|
cdd7c6fa2b | ||
|
|
c51dcfdad4 | ||
|
|
e68fe87e9e | ||
|
|
fca77c6bd8 | ||
| aa89a10aa8 | |||
| 21af3e3310 | |||
| 08faa9f6b0 | |||
| b101c63f8d | |||
| 41820ff2b3 | |||
| 9c045f32ea | |||
| 6dd8dcd06e | |||
| f79629e97e | |||
| b52bb57fbc | |||
| 401f8f13a2 | |||
| 7b0d4e5d30 | |||
| 0ac649345d |
@@ -19,6 +19,7 @@ import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
|
||||
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
|
||||
import ListPelayananPage from "./pages/scr/dashboard/pelayanan-surat/list_pelayanan_page";
|
||||
import ListPage from "./pages/scr/dashboard/pengaduan/list_page";
|
||||
import DetailPage from "./pages/scr/dashboard/pengaduan/detail_page";
|
||||
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
|
||||
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
|
||||
import ScrLayout from "./pages/scr/scr_layout";
|
||||
@@ -102,6 +103,10 @@ export default function AppRoutes() {
|
||||
path="/scr/dashboard/pengaduan/list"
|
||||
element={<ListPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/scr/dashboard/pengaduan/detail"
|
||||
element={<DetailPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/scr/dashboard/apikey/apikey"
|
||||
element={<ApikeyPage />}
|
||||
|
||||
@@ -21,6 +21,7 @@ const clientRoutes = {
|
||||
"/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home",
|
||||
"/scr/dashboard/pelayanan-surat/list-pelayanan": "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
||||
"/scr/dashboard/pengaduan/list": "/scr/dashboard/pengaduan/list",
|
||||
"/scr/dashboard/pengaduan/detail": "/scr/dashboard/pengaduan/detail",
|
||||
"/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey",
|
||||
"/dir/dir": "/dir/dir",
|
||||
"/*": "/*"
|
||||
|
||||
@@ -12,6 +12,7 @@ import LayananRoute from "./server/routes/layanan_route";
|
||||
import { MCPRoute } from "./server/routes/mcp_route";
|
||||
import PelayananRoute from "./server/routes/pelayanan_surat_route";
|
||||
import PengaduanRoute from "./server/routes/pengaduan_route";
|
||||
import TestRoute from "./server/routes/test";
|
||||
import UserRoute from "./server/routes/user_route";
|
||||
import cors from "@elysiajs/cors";
|
||||
|
||||
@@ -29,6 +30,7 @@ const Api = new Elysia({
|
||||
})
|
||||
.use(PengaduanRoute)
|
||||
.use(PelayananRoute)
|
||||
.use(TestRoute)
|
||||
.use(apiAuth)
|
||||
.use(ApiKeyRoute)
|
||||
.use(DarmasabaRoute)
|
||||
|
||||
301
src/pages/scr/dashboard/pengaduan/detail_page.tsx
Normal file
301
src/pages/scr/dashboard/pengaduan/detail_page.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Anchor,
|
||||
Badge,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconAlignJustified,
|
||||
IconCategory,
|
||||
IconFileCertificate,
|
||||
IconInfoTriangle,
|
||||
IconMapPin,
|
||||
IconMessageReport,
|
||||
IconPhotoScan,
|
||||
IconUser
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import useSwr from "swr";
|
||||
|
||||
export default function DetailPengaduanPage() {
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const id = query.get("id");
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataPengaduan />
|
||||
<DetailDataHistori />
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<DetailUserPengaduan />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataPengaduan() {
|
||||
return (
|
||||
<Card
|
||||
radius="md"
|
||||
p="lg"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Group gap="xs">
|
||||
<Title order={4} c="gray.2">
|
||||
Pengaduan
|
||||
</Title>
|
||||
<Title order={4} c="dimmed">
|
||||
#PGf-2345-33
|
||||
</Title>
|
||||
</Group>
|
||||
<Badge
|
||||
size="xl"
|
||||
variant="light"
|
||||
radius="sm"
|
||||
color={"yellow"}
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
antrian
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<Stack gap="md">
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconAlignJustified size={20} />
|
||||
<Text size="md">
|
||||
Judul
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c={"white"}>Judul Pengaduan</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={20} />
|
||||
<Text size="md">
|
||||
Lokasi
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">fwef</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Stack gap="md">
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconCategory size={20} />
|
||||
<Text size="md">
|
||||
Kategori
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">fwef</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconPhotoScan size={20} />
|
||||
<Text size="md">
|
||||
Gambar
|
||||
</Text>
|
||||
</Group>
|
||||
<Anchor href="https://mantine.dev/" target="_blank">
|
||||
Lihat Gambar
|
||||
</Anchor>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Stack gap="md">
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconAlignJustified size={20} />
|
||||
<Text size="md">
|
||||
Detail
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Illum, corporis iusto. Suscipit veritatis quas, non nobis fuga, laudantium accusantium tempora sint aliquid architecto totam esse eum excepturi nostrum fugiat ut.
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconInfoTriangle size={20} />
|
||||
<Text size="md">
|
||||
Keterangan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c={"white"}>
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At fugiat eligendi nesciunt dolore? Maiores a cumque vitae suscipit incidunt quos beatae modi, vel, id ullam quae voluptas, deserunt quas placeat.
|
||||
</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataHistori() {
|
||||
const elements = [
|
||||
{ position: 6, mass: 12.011, symbol: 'C', name: 'Carbon' },
|
||||
{ position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen' },
|
||||
{ position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium' },
|
||||
{ position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium' },
|
||||
{ position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium' },
|
||||
];
|
||||
|
||||
const rows = elements.map((element) => (
|
||||
<Table.Tr key={element.name}>
|
||||
<Table.Td>{element.position}</Table.Td>
|
||||
<Table.Td>{element.name}</Table.Td>
|
||||
<Table.Td>{element.symbol}</Table.Td>
|
||||
<Table.Td>{element.mass}</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
return (
|
||||
<Card
|
||||
radius="md"
|
||||
p="lg"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Histori Pengaduan
|
||||
</Title>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Tanggal</Table.Th>
|
||||
<Table.Th>Deskripsi</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>User</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
function DetailUserPengaduan() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
apiFetch.api.pengaduan.list.get({
|
||||
query: {
|
||||
status,
|
||||
search: value,
|
||||
take: "",
|
||||
page: "",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
|
||||
const list = data?.data || [];
|
||||
|
||||
return (
|
||||
<Card
|
||||
radius="md"
|
||||
p="lg"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Flex align="center" justify="space-between">
|
||||
<Flex direction={"column"}>
|
||||
<Title order={4} c="gray.2">
|
||||
Warga
|
||||
</Title>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" >
|
||||
<Group gap="xs">
|
||||
<IconUser size={20} />
|
||||
<Text size="md">
|
||||
Nama
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c={"white"}>Amalia Dwi Yustiani</Text>
|
||||
</Group>
|
||||
<Group justify="space-between" >
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={20} />
|
||||
<Text size="md">
|
||||
Telepon
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">08123456789</Text>
|
||||
</Group>
|
||||
<Group justify="space-between" >
|
||||
<Group gap="xs">
|
||||
<IconMessageReport size={20} />
|
||||
<Text size="md">
|
||||
Jumlah Pengaduan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">10</Text>
|
||||
</Group>
|
||||
<Group justify="space-between" >
|
||||
<Group gap="xs">
|
||||
<IconFileCertificate size={20} />
|
||||
<Text size="md">
|
||||
Jumlah Pelayanan Surat
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">10</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -115,7 +115,9 @@ type StatusKey =
|
||||
| "ditolak"
|
||||
| "selesai"
|
||||
| "semua";
|
||||
|
||||
function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
const navigate = useNavigate();
|
||||
const [page, setPage] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
@@ -196,6 +198,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
onClick={() => navigate(`/scr/dashboard/pengaduan/detail?id=${v.id}`)}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Flex align="center" justify="space-between">
|
||||
|
||||
42
src/server/lib/mimetypeToExtension.ts
Normal file
42
src/server/lib/mimetypeToExtension.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export function mimeToExtension(mimeType: string): string {
|
||||
const map: Record<string, string> = {
|
||||
"image/jpeg": "jpg",
|
||||
"image/png": "png",
|
||||
"image/gif": "gif",
|
||||
"image/webp": "webp",
|
||||
"image/svg+xml": "svg",
|
||||
"image/bmp": "bmp",
|
||||
"image/tiff": "tiff",
|
||||
|
||||
"video/mp4": "mp4",
|
||||
"video/webm": "webm",
|
||||
"video/ogg": "ogv",
|
||||
"video/quicktime": "mov",
|
||||
|
||||
"audio/mpeg": "mp3",
|
||||
"audio/wav": "wav",
|
||||
"audio/ogg": "ogg",
|
||||
"audio/webm": "weba",
|
||||
|
||||
"application/pdf": "pdf",
|
||||
"application/zip": "zip",
|
||||
"application/x-zip-compressed": "zip",
|
||||
"application/json": "json",
|
||||
"application/javascript": "js",
|
||||
"application/x-httpd-php": "php",
|
||||
"application/msword": "doc",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
||||
"application/vnd.ms-excel": "xls",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
||||
"application/vnd.ms-powerpoint": "ppt",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
||||
|
||||
"text/plain": "txt",
|
||||
"text/html": "html",
|
||||
"text/css": "css",
|
||||
"text/csv": "csv",
|
||||
"text/xml": "xml",
|
||||
};
|
||||
|
||||
return map[mimeType.toLowerCase()] || "bin"; // default jika tidak dikenal
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export async function uploadFile(config: Config, file: File): Promise<string> {
|
||||
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||
return `✅ Uploaded ${file.name} successfully`;
|
||||
}
|
||||
export async function uploadFileBase64(config: Config, base64File: { name: string; data: string }): Promise<string> {
|
||||
export async function uploadFileBase64(config: Config, base64File: { name: string; data: string; }): Promise<string> {
|
||||
const remoteName = path.basename(base64File.name);
|
||||
|
||||
// 1. Dapatkan upload link (pakai Authorization)
|
||||
|
||||
@@ -38,10 +38,7 @@ const PelayananRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pelayanan surat sudah dibuat`
|
||||
return { success: true, message: 'kategori pelayanan surat sudah dibuat' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name harus diisi" }),
|
||||
@@ -67,10 +64,7 @@ const PelayananRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pelayanan surat sudah diperbarui`
|
||||
return { success: true, message: 'kategori pelayanan surat sudah diperbarui' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
@@ -95,10 +89,7 @@ const PelayananRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pelayanan surat sudah dihapus`
|
||||
return { success: true, message: 'kategori pelayanan surat sudah dihapus' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
@@ -252,10 +243,7 @@ const PelayananRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
pengaduan sudah dibuat`
|
||||
return { success: true, message: 'pengajuan surat sudah dibuat' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
|
||||
@@ -300,10 +288,7 @@ const PelayananRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
pengajuan surat sudah diperbarui`
|
||||
return { success: true, message: 'pengajuan surat sudah diperbarui' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Elysia, { t } from "elysia"
|
||||
import type { StatusPengaduan } from "generated/prisma"
|
||||
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 { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { prisma } from "../lib/prisma"
|
||||
@@ -38,10 +40,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pengaduan sudah dibuat`
|
||||
return { success: true, message: 'kategori pengaduan sudah dibuat' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name harus diisi" }),
|
||||
@@ -63,10 +62,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pengaduan sudah diperbarui`
|
||||
return { success: true, message: 'kategori pengaduan sudah diperbarui' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
@@ -89,10 +85,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pengaduan sudah dihapus`
|
||||
return { success: true, message: 'kategori pengaduan sudah dihapus' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
@@ -189,10 +182,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
pengaduan sudah dibuat`
|
||||
return { success: true, message: 'pengaduan sudah dibuat' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
title: t.String({ minLength: 1, error: "title harus diisi" }),
|
||||
@@ -246,10 +236,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
status pengaduan sudah diupdate`
|
||||
return { success: true, message: 'status pengaduan sudah diupdate' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
@@ -463,31 +450,35 @@ const PengaduanRoute = new Elysia({
|
||||
},
|
||||
})
|
||||
.post("/upload-base64", async ({ body }) => {
|
||||
const { file } = body;
|
||||
const { data, mimetype } = body;
|
||||
const ext = mimeToExtension(mimetype)
|
||||
const name = `${uuidv4()}.${ext}`
|
||||
|
||||
// Validasi file
|
||||
if (!file) {
|
||||
if (!data) {
|
||||
return { success: false, message: "File tidak ditemukan" };
|
||||
}
|
||||
|
||||
// Konversi file ke base64
|
||||
const buffer = await file.arrayBuffer();
|
||||
const base64String = Buffer.from(buffer).toString("base64");
|
||||
// const buffer = await file.arrayBuffer();
|
||||
// const base64String = Buffer.from(buffer).toString("base64");
|
||||
|
||||
// (Opsional) jika perlu dikirim ke Seafile sebagai base64
|
||||
const result = await uploadFileBase64(defaultConfigSF, { name: file.name, data: base64String });
|
||||
const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Upload berhasil",
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
base64Preview: base64String.slice(0, 100) + "...", // hanya preview
|
||||
seafileResult: result
|
||||
data: {
|
||||
name,
|
||||
mimetype,
|
||||
ext,
|
||||
}
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.File({ format: "binary" })
|
||||
data: t.String(),
|
||||
mimetype: t.String()
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File (Base64)",
|
||||
@@ -572,6 +563,7 @@ const PengaduanRoute = new Elysia({
|
||||
const dataFix = data.map((item) => {
|
||||
return {
|
||||
noPengaduan: item.noPengaduan,
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
detail: item.detail,
|
||||
status: item.status,
|
||||
|
||||
53
src/server/routes/test.ts
Normal file
53
src/server/routes/test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
|
||||
const TestRoute = new Elysia({
|
||||
prefix: "test",
|
||||
tags: ["mcp", "test"],
|
||||
})
|
||||
.get("/info-rapat-list", () => {
|
||||
return {
|
||||
success: true,
|
||||
message: "data info rapat berhasil diambil",
|
||||
data: [
|
||||
{
|
||||
judul: "Info Rapat",
|
||||
tanggal: "2025-11-10",
|
||||
deskripsi: "Info rapat",
|
||||
gambar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII="
|
||||
}
|
||||
]
|
||||
}
|
||||
}, {
|
||||
detail: {
|
||||
summary: "mendapatkan list rapat",
|
||||
description: "mendapatkan list rapat dari database",
|
||||
}
|
||||
})
|
||||
|
||||
.post("/simpan-rapat", ({ body }) => {
|
||||
if (!body.gambar) {
|
||||
return {
|
||||
success: false,
|
||||
message: "gambar harus diisi",
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: "data info rapat berhasil diambil",
|
||||
chunk: body.gambar.substring(22)
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
judul: t.String(),
|
||||
tanggal: t.String(),
|
||||
deskripsi: t.String(),
|
||||
gambar: t.Required(t.String()),
|
||||
}),
|
||||
detail: {
|
||||
summary: "simpan data rapat",
|
||||
description: "simpan data rapat memerlukan base64 gambar",
|
||||
}
|
||||
})
|
||||
|
||||
export default TestRoute
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
IMAGE_BASE64=$(base64 image.png | tr -d '\n')
|
||||
|
||||
curl -X POST http://localhost:3000/api/pengaduan/upload-base64 \
|
||||
-F file=@package.json
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"file\": \"$IMAGE_BASE64\"}"
|
||||
|
||||
154
x.ts
154
x.ts
@@ -1,133 +1,39 @@
|
||||
/**
|
||||
* src/utils/swagger-to-mcp.ts
|
||||
*
|
||||
* Auto-converter: Swagger (OpenAPI) → MCP manifest (real-time)
|
||||
*
|
||||
* - Fetch swagger JSON dynamically from process.env.BUN_PUBLIC_BASE_URL + "/docs/json"
|
||||
* - Generate MCP manifest for AI discovery (/.well-known/mcp.json)
|
||||
* - Can be used as Bun CLI or integrated in Elysia route
|
||||
*/
|
||||
import fs from "fs";
|
||||
|
||||
import { writeFileSync } from "fs"
|
||||
// 1️⃣ File yang mau diupload
|
||||
const filePath = "image.png";
|
||||
const apiUrl = "http://localhost:3000/api/pengaduan/upload-base64";
|
||||
|
||||
interface OpenAPI {
|
||||
info: { title?: string; description?: string; version?: string }
|
||||
paths: Record<string, any>
|
||||
}
|
||||
// 2️⃣ Baca file dan ubah ke base64
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
const base64Data = fileBuffer.toString("base64");
|
||||
|
||||
interface McpManifest {
|
||||
schema_version: string
|
||||
name: string
|
||||
description: string
|
||||
version?: string
|
||||
endpoints: Record<string, string>
|
||||
capabilities: Record<string, any>
|
||||
contact?: { email?: string }
|
||||
}
|
||||
// 3️⃣ Buat payload JSON
|
||||
const payload = {
|
||||
data: base64Data,
|
||||
mimetype: "image/png"
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert OpenAPI JSON to MCP manifest format
|
||||
*/
|
||||
async function convertOpenApiToMcp(baseUrl: string): Promise<McpManifest> {
|
||||
const res = await fetch(`${baseUrl}/docs/json`)
|
||||
if (!res.ok) throw new Error(`Failed to fetch Swagger JSON from ${baseUrl}/docs/json`)
|
||||
// 4️⃣ Kirim ke server pakai fetch
|
||||
async function uploadBase64() {
|
||||
try {
|
||||
const res = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const openapi: OpenAPI = await res.json()
|
||||
|
||||
const manifest: McpManifest = {
|
||||
schema_version: "1.0",
|
||||
name: openapi.info?.title ?? "MCP Server",
|
||||
description: openapi.info?.description ?? "Auto-generated MCP manifest from Swagger",
|
||||
version: openapi.info?.version ?? "0.0.0",
|
||||
endpoints: {
|
||||
openapi: `${baseUrl}/docs/json`,
|
||||
mcp: `${baseUrl}/.well-known/mcp.json`
|
||||
},
|
||||
capabilities: {}
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
for (const [path, methods] of Object.entries(openapi.paths || {})) {
|
||||
for (const [method, def] of Object.entries<any>(methods)) {
|
||||
const tags = def.tags || ["default"]
|
||||
const tag = tags[0]
|
||||
const operationId = def.operationId || `${method}_${path.replace(/[\/{}]/g, "_")}`
|
||||
|
||||
manifest.capabilities[tag] ??= {}
|
||||
|
||||
// Extract parameters and body schema
|
||||
const params: Record<string, string> = {}
|
||||
const required: string[] = []
|
||||
|
||||
if (Array.isArray(def.parameters)) {
|
||||
for (const p of def.parameters) {
|
||||
const type = p.schema?.type || "string"
|
||||
params[p.name] = type
|
||||
if (p.required) required.push(p.name)
|
||||
}
|
||||
}
|
||||
|
||||
const bodySchema = def.requestBody?.content?.["application/json"]?.schema
|
||||
if (bodySchema?.properties) {
|
||||
for (const [key, prop] of Object.entries<any>(bodySchema.properties)) {
|
||||
params[key] = prop.type || "string"
|
||||
}
|
||||
if (Array.isArray(bodySchema.required))
|
||||
required.push(...bodySchema.required)
|
||||
}
|
||||
|
||||
// Generate example cURL
|
||||
const sampleCurl = [
|
||||
`curl -X ${method.toUpperCase()} ${baseUrl}${path}`,
|
||||
Object.keys(params).length > 0
|
||||
? ` -H 'Content-Type: application/json' -d '${JSON.stringify(
|
||||
Object.fromEntries(Object.keys(params).map(k => [k, params[k] === "string" ? k : "value"]))
|
||||
)}'`
|
||||
: ""
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" \\\n")
|
||||
|
||||
manifest.capabilities[tag][operationId] = {
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
summary: def.summary || def.description || "",
|
||||
parameters: Object.keys(params).length > 0 ? params : undefined,
|
||||
required: required.length > 0 ? required : undefined,
|
||||
command: sampleCurl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return manifest
|
||||
const result = await res.json();
|
||||
console.log("✅ Upload sukses:", result);
|
||||
} catch (err) {
|
||||
console.error("❌ Upload gagal:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI entry
|
||||
* bun run src/utils/swagger-to-mcp.ts
|
||||
*/
|
||||
if (import.meta.main) {
|
||||
const baseUrl = process.env.BUN_PUBLIC_BASE_URL
|
||||
if (!baseUrl) {
|
||||
console.error("❌ Missing BUN_PUBLIC_BASE_URL environment variable.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
convertOpenApiToMcp(baseUrl)
|
||||
.then(manifest => {
|
||||
writeFileSync(".well-known/mcp.json", JSON.stringify(manifest, null, 2))
|
||||
console.log("✅ Generated .well-known/mcp.json")
|
||||
})
|
||||
.catch(err => console.error("❌ Failed to convert Swagger → MCP:", err))
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional: Elysia integration
|
||||
* Automatically serve /.well-known/mcp.json
|
||||
*/
|
||||
// import Elysia from "elysia"
|
||||
// new Elysia()
|
||||
// .get("/.well-known/mcp.json", async () => {
|
||||
// const baseUrl = process.env.BUN_PUBLIC_BASE_URL!
|
||||
// return await convertOpenApiToMcp(baseUrl)
|
||||
// })
|
||||
// .listen(3000)
|
||||
uploadBase64();
|
||||
|
||||
Reference in New Issue
Block a user