diff --git a/src/App.tsx b/src/App.tsx index 774136b..69863ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import "@mantine/core/styles.css"; -import "@mantine/notifications/styles.css"; import "@mantine/dates/styles.css"; import { Notifications } from "@mantine/notifications"; +import "@mantine/notifications/styles.css"; import { MantineProvider } from "@mantine/core"; import AppRoutes from "./AppRoutes"; diff --git a/src/components/DesaSetting.tsx b/src/components/DesaSetting.tsx new file mode 100644 index 0000000..fa9e3f3 --- /dev/null +++ b/src/components/DesaSetting.tsx @@ -0,0 +1,157 @@ +import apiFetch from "@/lib/apiFetch"; +import { + ActionIcon, + Button, + Divider, + Flex, + Group, + Input, + Modal, + Stack, + Table, + Title, + Tooltip +} from "@mantine/core"; +import { useDisclosure, useShallowEffect } from "@mantine/hooks"; +import { IconEdit } from "@tabler/icons-react"; +import { useState } from "react"; +import useSWR from "swr"; +import notification from "./notificationGlobal"; + +export default function DesaSetting() { + const [btnDisable, setBtnDisable] = useState(false); + const [btnLoading, setBtnLoading] = useState(false); + const [opened, { open, close }] = useDisclosure(false); + const { data, mutate, isLoading } = useSWR("/", () => + apiFetch.api["configuration-desa"].list.get(), + ); + const list = data?.data || []; + const [dataEdit, setDataEdit] = useState({ + id: "", + value: "", + name: "", + }); + + useShallowEffect(() => { + mutate(); + }, []); + + async function handleEdit() { + try { + setBtnLoading(true); + const res = await apiFetch.api["configuration-desa"].edit.post(dataEdit); + if (res.status === 200) { + mutate(); + close(); + notification({ + title: "Success", + message: "Your settings have been saved", + type: "success", + }) + } else { + notification({ + title: "Error", + message: "Failed to edit configuration", + type: "error", + }) + } + } catch (error) { + console.log(error); + notification({ + title: "Error", + message: "Failed to edit configuration", + type: "error", + }) + } finally { + setBtnLoading(false); + } + } + + function chooseEdit({ data }: { data: { id: string, value: string, name: string } }) { + setDataEdit(data); + open(); + } + + function onValidation({ kat, value }: { kat: 'value', value: string }) { + if (value.length < 1) { + setBtnDisable(true); + } else { + setBtnDisable(false); + } + + if (kat === 'value') { + setDataEdit({ ...dataEdit, value: value }); + } + } + + useShallowEffect(() => { + if (dataEdit.value.length > 0) { + setBtnDisable(false); + } + }, [dataEdit.id]); + + return ( + <> + + + + onValidation({ kat: 'value', value: e.target.value })} /> + + + + + + + + + + + Pengaturan Desa + + + + + + + + Nama + Value + Aksi + + + + {list?.map((v: any) => ( + + {v.name} + {v.value} + + + chooseEdit({ data: v })} + > + + + + + + ))} + +
+
+
+ + ); +} diff --git a/src/components/notificationGlobal.ts b/src/components/notificationGlobal.ts new file mode 100644 index 0000000..2363a8a --- /dev/null +++ b/src/components/notificationGlobal.ts @@ -0,0 +1,38 @@ +import { showNotification } from "@mantine/notifications"; + +export default function notification({ title, message, type }: { title: string, message: string, type: "success" | "error" | "warning" | "info" }) { + switch (type) { + case "success": + return showNotification({ + title, + message, + color: "green", + autoClose: 3000, + }) + break; + case "error": + return showNotification({ + title, + message, + color: "red", + autoClose: 3000, + }) + break; + case "warning": + return showNotification({ + title, + message, + color: "orange", + autoClose: 3000, + }) + break; + case "info": + return showNotification({ + title, + message, + color: "blue", + autoClose: 3000, + }) + break; + } +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 7f6390e..202417e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,4 @@ +import cors from "@elysiajs/cors"; import Swagger from "@elysiajs/swagger"; import Elysia from "elysia"; import html from "./index.html"; @@ -14,7 +15,7 @@ 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"; +import ConfigurationDesaRoute from "./server/routes/configuration_desa_route"; const Docs = new Elysia({ tags: ["docs"], @@ -30,6 +31,7 @@ const Api = new Elysia({ }) .use(PengaduanRoute) .use(PelayananRoute) + .use(ConfigurationDesaRoute) .use(TestRoute) .use(apiAuth) .use(ApiKeyRoute) diff --git a/src/pages/scr/dashboard/setting/detail_setting_page.tsx b/src/pages/scr/dashboard/setting/detail_setting_page.tsx index ec6e714..230e5ae 100644 --- a/src/pages/scr/dashboard/setting/detail_setting_page.tsx +++ b/src/pages/scr/dashboard/setting/detail_setting_page.tsx @@ -1,150 +1,171 @@ -import { Button, Card, Container, Divider, Flex, Grid, Group, Input, NavLink, Stack, Table, Title } from "@mantine/core"; -import { IconCircleOff, IconGauge, IconHome2 } from "@tabler/icons-react"; +import DesaSetting from "@/components/DesaSetting"; +import { + Button, + Card, + Container, + Divider, + Flex, + Grid, + Group, + Input, + NavLink, + Stack, + Table, + Title, +} from "@mantine/core"; +import { + IconBuildingBank, + IconCategory2, + IconMailSpark, + IconUserCog, +} from "@tabler/icons-react"; import { useLocation } from "react-router-dom"; export default function DetailSettingPage() { - const { search } = useLocation(); - const query = new URLSearchParams(search); - const type = query.get("type"); + const { search } = useLocation(); + const query = new URLSearchParams(search); + const type = query.get("type"); - return ( - - - - - } - active={type === "profile" || !type} - /> - } - active={type === "cat-pengaduan"} - /> - } - active={type === "cat-pelayanan"} - /> - } - active={type === "desa"} - /> - - - - - {type === "cat-pengaduan" - ? - : type === "cat-pelayanan" - ? - : type === "desa" - ? - : } - - - - - ); + return ( + + + + + } + active={type === "profile" || !type} + /> + } + active={type === "cat-pengaduan"} + /> + } + active={type === "cat-pelayanan"} + /> + } + active={type === "desa"} + /> + + + + + {type === "cat-pengaduan" ? ( + + ) : type === "cat-pelayanan" ? ( + + ) : type === "desa" ? ( + + ) : ( + + )} + + + + + ); } function ProfilePage() { - return ( + return ( + + + + Profile Pengguna + + + + - - - Profile Pengguna - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - ) + + ); } function KategoriPengaduanPage() { - 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 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) => ( - - {element.position} - {element.name} - {element.symbol} - {element.mass} - - )); - return ( + const rows = elements.map((element) => ( + + {element.position} + {element.name} + {element.symbol} + {element.mass} + + )); + return ( + + + + Kategori Pengaduan + + + + - - - Kategori Pengaduan - - - - - - - - - Tanggal - Deskripsi - Status - User - - - {rows} -
-
+ + + + Tanggal + Deskripsi + Status + User + + + {rows} +
- ) +
+ ); } diff --git a/src/server/lib/seafile.ts b/src/server/lib/seafile.ts index 5c09a8e..3aed792 100644 --- a/src/server/lib/seafile.ts +++ b/src/server/lib/seafile.ts @@ -162,6 +162,7 @@ export async function uploadFile(config: Config, file: File): Promise { 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 { const remoteName = path.basename(base64File.name); @@ -194,6 +195,38 @@ export async function uploadFileBase64(config: Config, base64File: { name: strin return `✅ Uploaded ${base64File.name} successfully`; } +export async function uploadFileToFolder(config: Config, base64File: { name: string; data: string; }, folder: 'syarat-dokumen' | 'pengaduan'): Promise { + const remoteName = path.basename(base64File.name); + + // 1. Dapatkan upload link (pakai Authorization) + const uploadUrlResponse = await fetchWithAuth( + config, + `${config.URL}/${config.REPO}/upload-link/` + ); + const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, ""); + + // 2. Konversi base64 ke Blob + const binary = Buffer.from(base64File.data, "base64"); + const blob = new Blob([binary]); + + // 3. Siapkan form-data + const formData = new FormData(); + formData.append("parent_dir", "/"); + formData.append("relative_path", folder); // tanpa slash di akhir + formData.append("file", blob, remoteName); + + // 4. Upload file TANPA Authorization header, token di query param + const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, { + method: "POST", + body: formData, + }); + + const text = await res.text(); + + if (!res.ok) throw new Error(`Upload failed: ${text}`); + return `✅ Uploaded ${base64File.name} successfully`; +} + diff --git a/src/server/routes/configuration_desa_route.ts b/src/server/routes/configuration_desa_route.ts new file mode 100644 index 0000000..f6142f3 --- /dev/null +++ b/src/server/routes/configuration_desa_route.ts @@ -0,0 +1,48 @@ +import Elysia, { t } from "elysia"; +import { prisma } from "../lib/prisma"; + +const ConfigurationDesaRoute = new Elysia({ + prefix: "configuration-desa", + tags: ["configuration-desa"], +}) + + .get("/list", async () => { + const data = await prisma.configuration.findMany({ + orderBy: { + name: "asc" + } + }) + + return data + }, { + detail: { + summary: "List Konfigurasi", + description: `tool untuk mendapatkan list konfigurasi`, + } + }) + .post("/edit", async ({ body }) => { + const { id, value } = body + + await prisma.configuration.update({ + where: { + id, + }, + data: { + value, + } + }) + + return { success: true, message: 'konfigurasi sudah diperbarui' } + }, { + body: t.Object({ + id: t.String({ minLength: 1, error: "id harus diisi" }), + value: t.String({ minLength: 1, error: "value harus diisi" }), + }), + detail: { + summary: "edit konfigurasi desa", + description: `tool untuk edit konfigurasi desa` + } + }) + ; + +export default ConfigurationDesaRoute diff --git a/src/server/routes/pengaduan_route.ts b/src/server/routes/pengaduan_route.ts index 966a033..358c864 100644 --- a/src/server/routes/pengaduan_route.ts +++ b/src/server/routes/pengaduan_route.ts @@ -6,7 +6,7 @@ import { mimeToExtension } from "../lib/mimetypeToExtension" import { generateNoPengaduan } from "../lib/no-pengaduan" import { normalizePhoneNumber } from "../lib/normalizePhone" import { prisma } from "../lib/prisma" -import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64 } from "../lib/seafile" +import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64, uploadFileToFolder } from "../lib/seafile" const PengaduanRoute = new Elysia({ prefix: "pengaduan", @@ -100,7 +100,8 @@ const PengaduanRoute = new Elysia({ // --- PENGADUAN --- .post("/create", async ({ body }) => { - const { title, detail, location, image, idCategory, idWarga, phone } = body + const { title, detail, location, imageData, imageMime, idCategory, idWarga, phone } = body + let imageFix = null const noPengaduan = await generateNoPengaduan() let idCategoryFix = idCategory let idWargaFix = idWarga @@ -110,6 +111,12 @@ const PengaduanRoute = new Elysia({ } }) + if (!imageData && !imageMime) { + const ext = mimeToExtension(imageMime) + imageFix = `${uuidv4()}.${ext}` + await uploadFileToFolder(defaultConfigSF, { name: imageFix, data: imageData }, "pengaduan") + } + if (!category) { const cariCategory = await prisma.categoryPengaduan.findFirst({ where: { @@ -163,7 +170,7 @@ const PengaduanRoute = new Elysia({ idCategory: idCategoryFix, idWarga: idWargaFix, location, - image, + image: imageFix, noPengaduan, }, select: { @@ -172,7 +179,7 @@ const PengaduanRoute = new Elysia({ }) if (!pengaduan.id) { - throw new Error("gagal membuat pengaduan") + return { success: false, message: 'gagal membuat pengaduan' } } await prisma.historyPengaduan.create({ @@ -188,7 +195,8 @@ const PengaduanRoute = new Elysia({ title: t.String({ minLength: 1, error: "title harus diisi" }), detail: t.String({ minLength: 1, error: "detail harus diisi" }), location: t.String({ minLength: 1, error: "location harus diisi" }), - image: t.Any(), + imageData: t.String({ optional: true, description: "base64 encoded image data" }), + imageMime: t.String({ optional: true, description: "mime type of image" }), idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }), idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }), phone: t.String({ minLength: 1, error: "phone harus diisi" }), @@ -622,7 +630,7 @@ const PengaduanRoute = new Elysia({ const { fileName } = query const connect = await testConnection(defaultConfigSF) - console.log({connect}) + console.log({ connect }) const hasil = await catFile(defaultConfigSF, fileName) console.log('hasilnya', hasil)