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)