Compare commits

...

7 Commits

Author SHA1 Message Date
5101a21f54 upd: api mcp pengaduan
Deskripsi:
- bahasa nya lebih bisa di mengerti ai bukan bahasa developer

No Issues
2025-11-12 17:41:00 +08:00
503c3e330d upd: dashboard admin
Deskripsi:
- tampilan list kategori pengaduan
- tambah kategori pengaduan
- edit kategori pengaduan

No Issues
2025-11-12 17:20:14 +08:00
a4167cfc8b upd: coba api create pengaduan dengan gambar 2025-11-12 15:35:41 +08:00
14e2d711b3 upd: api create pengaduan dg image 2025-11-12 15:19:19 +08:00
63c88161d3 upd: coba api pengaduan dengan upload gambar 2025-11-12 14:57:22 +08:00
eacc8fc220 update: dashboard admin
Deskripsi:
- list data konfigurasi api
- edit data konfigurasi api
- integrasi api

No Issues
2025-11-12 14:40:13 +08:00
422ca5a2cc Merge pull request 'amalia/11-nov-25' (#19) from amalia/11-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/19
2025-11-11 17:49:00 +08:00
9 changed files with 758 additions and 161 deletions

View File

@@ -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";

View File

@@ -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 (
<>
<Modal
opened={opened}
onClose={close}
title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label={dataEdit.name}>
<Input value={dataEdit.value} onChange={(e) => onValidation({ kat: 'value', value: e.target.value })} />
</Input.Wrapper>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" onClick={handleEdit} disabled={btnDisable} loading={btnLoading}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Pengaturan Desa
</Title>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Nama</Table.Th>
<Table.Th>Value</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>{v.value}</Table.Td>
<Table.Td>
<Tooltip label="Edit Setting">
<ActionIcon
variant="light"
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Stack>
</Stack>
</>
);
}

View File

@@ -0,0 +1,234 @@
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, IconPlus } from "@tabler/icons-react";
import { useState } from "react";
import useSWR from "swr";
import notification from "./notificationGlobal";
export default function KategoriPengaduan() {
const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const [openedTambah, { open: openTambah, close: closeTambah }] = useDisclosure(false);
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api.pengaduan.category.get(),
);
const list = data?.data || [];
const [dataEdit, setDataEdit] = useState({
id: "",
name: "",
});
const [dataTambah, setDataTambah] = useState("")
useShallowEffect(() => {
mutate();
}, []);
async function handleCreate() {
try {
setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.create.post({ name: dataTambah });
if (res.status === 200) {
mutate();
closeTambah();
setDataTambah("");
notification({
title: "Success",
message: "Your category have been saved",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to create category",
type: "error",
})
}
} catch (error) {
console.log(error);
notification({
title: "Error",
message: "Failed to create category",
type: "error",
})
} finally {
setBtnLoading(false);
}
}
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your category have been saved",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to edit category",
type: "error",
})
}
} catch (error) {
console.log(error);
notification({
title: "Error",
message: "Failed to edit category",
type: "error",
})
} finally {
setBtnLoading(false);
}
}
function chooseEdit({ data }: { data: { id: string, value: string, name: string } }) {
setDataEdit(data);
open();
}
function onValidation({ kat, value, aksi }: { kat: 'name', value: string, aksi: 'edit' | 'tambah' }) {
if (value.length < 1) {
setBtnDisable(true);
} else {
setBtnDisable(false);
}
if (kat === 'name') {
if (aksi === 'edit') {
setDataEdit({ ...dataEdit, name: value });
} else {
setDataTambah(value);
}
}
}
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
setBtnDisable(false);
}
}, [dataEdit.id]);
useShallowEffect(() => {
if (dataTambah.length > 0) {
setBtnDisable(false);
}
}, [dataTambah]);
return (
<>
{/* Modal Edit */}
<Modal
opened={opened}
onClose={close}
title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label="Edit Kategori">
<Input value={dataEdit.name} onChange={(e) => onValidation({ kat: 'name', value: e.target.value, aksi: 'edit' })} />
</Input.Wrapper>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" onClick={handleEdit} disabled={btnDisable} loading={btnLoading}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Tambah */}
<Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label="Tambah Kategori">
<Input value={dataTambah} onChange={(e) => onValidation({ kat: 'name', value: e.target.value, aksi: 'tambah' })} />
</Input.Wrapper>
<Group justify="center" grow>
<Button variant="light" onClick={closeTambah}>
Batal
</Button>
<Button variant="filled" onClick={handleCreate} disabled={btnDisable} loading={btnLoading}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Kategori Pengaduan
</Title>
<Tooltip label="Tambah Kategori Pengaduan">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Kategori</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>
<Tooltip label="Edit Setting">
<ActionIcon
variant="light"
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Stack>
</Stack>
</>
);
}

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -1,150 +1,172 @@
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 KategoriPengaduan from "@/components/KategoriPengaduan";
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 (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={3}>
<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)",
}}
>
<NavLink
href={`?type=profile`}
label="Profile"
leftSection={<IconHome2 size={16} stroke={1.5} />}
active={type === "profile" || !type}
/>
<NavLink
href={`?type=cat-pengaduan`}
label="Kategori Pengaduan"
leftSection={<IconGauge size={16} stroke={1.5} />}
active={type === "cat-pengaduan"}
/>
<NavLink
href={`?type=cat-pelayanan`}
label="Kategori Pelayanan Surat"
leftSection={<IconCircleOff size={16} stroke={1.5} />}
active={type === "cat-pelayanan"}
/>
<NavLink
href={`?type=desa`}
label="Desa"
leftSection={<IconCircleOff size={16} stroke={1.5} />}
active={type === "desa"}
/>
</Card>
</Grid.Col>
<Grid.Col span={9}>
<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)",
}}
>
{type === "cat-pengaduan"
? <KategoriPengaduanPage />
: type === "cat-pelayanan"
? <KategoriPengaduanPage />
: type === "desa"
? <KategoriPengaduanPage />
: <ProfilePage />}
</Card>
</Grid.Col>
</Grid>
</Container>
);
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={3}>
<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)",
}}
>
<NavLink
href={`?type=profile`}
label="Profile"
leftSection={<IconUserCog size={16} stroke={1.5} />}
active={type === "profile" || !type}
/>
<NavLink
href={`?type=cat-pengaduan`}
label="Kategori Pengaduan"
leftSection={<IconCategory2 size={16} stroke={1.5} />}
active={type === "cat-pengaduan"}
/>
<NavLink
href={`?type=cat-pelayanan`}
label="Kategori Pelayanan Surat"
leftSection={<IconMailSpark size={16} stroke={1.5} />}
active={type === "cat-pelayanan"}
/>
<NavLink
href={`?type=desa`}
label="Desa"
leftSection={<IconBuildingBank size={16} stroke={1.5} />}
active={type === "desa"}
/>
</Card>
</Grid.Col>
<Grid.Col span={9}>
<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)",
}}
>
{type === "cat-pengaduan" ? (
<KategoriPengaduan />
) : type === "cat-pelayanan" ? (
<KategoriPengaduanPage />
) : type === "desa" ? (
<DesaSetting />
) : (
<ProfilePage />
)}
</Card>
</Grid.Col>
</Grid>
</Container>
);
}
function ProfilePage() {
return (
return (
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Profile Pengguna
</Title>
<Button variant="light">Edit</Button>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Profile Pengguna
</Title>
<Button variant="light">Edit</Button>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Group gap="xl" grow>
<Input.Wrapper label="Nama" description="" error="">
<Input value={"Amalia Dwi Yustiani"} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Phone" description="" error="">
<Input value={"08123456789"} readOnly />
</Input.Wrapper>
</Group>
<Group gap="xl" grow>
<Input.Wrapper label="Email" description="" error="">
<Input value={"amaliadwiyustiani@gmail.com"} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Role" description="" error="">
<Input value={"Admin"} readOnly />
</Input.Wrapper>
</Group>
</Stack>
<Group gap="xl" grow>
<Input.Wrapper label="Nama" description="" error="">
<Input value={"Amalia Dwi Yustiani"} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Phone" description="" error="">
<Input value={"08123456789"} readOnly />
</Input.Wrapper>
</Group>
<Group gap="xl" grow>
<Input.Wrapper label="Email" description="" error="">
<Input value={"amaliadwiyustiani@gmail.com"} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Role" description="" error="">
<Input value={"Admin"} readOnly />
</Input.Wrapper>
</Group>
</Stack>
)
</Stack>
);
}
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) => (
<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 (
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 (
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Kategori Pengaduan
</Title>
<Button variant="light">Tambah</Button>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Kategori Pengaduan
</Title>
<Button variant="light">Tambah</Button>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<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>
<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>
)
</Stack>
);
}

View File

@@ -162,6 +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> {
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<string> {
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`;
}

View File

@@ -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

View File

@@ -100,20 +100,21 @@ const PengaduanRoute = new Elysia({
// --- PENGADUAN ---
.post("/create", async ({ body }) => {
const { title, detail, location, image, idCategory, idWarga, phone } = body
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
let imageFix = namaGambar
const noPengaduan = await generateNoPengaduan()
let idCategoryFix = idCategory
let idWargaFix = idWarga
let idCategoryFix = kategoriId
let idWargaFix = wargaId
const category = await prisma.categoryPengaduan.findUnique({
where: {
id: idCategory,
id: kategoriId,
}
})
if (!category) {
const cariCategory = await prisma.categoryPengaduan.findFirst({
where: {
name: idCategory,
name: kategoriId,
}
})
@@ -127,12 +128,12 @@ const PengaduanRoute = new Elysia({
const warga = await prisma.warga.findUnique({
where: {
id: idWarga,
id: wargaId,
}
})
if (!warga) {
const nomorHP = normalizePhoneNumber({ phone })
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const cariWarga = await prisma.warga.findUnique({
where: {
phone: nomorHP,
@@ -142,7 +143,7 @@ const PengaduanRoute = new Elysia({
if (!cariWarga) {
const wargaCreate = await prisma.warga.create({
data: {
name: idWarga,
name: wargaId,
phone: nomorHP,
},
select: {
@@ -158,12 +159,12 @@ const PengaduanRoute = new Elysia({
const pengaduan = await prisma.pengaduan.create({
data: {
title,
detail,
title: judulPengaduan,
detail: detailPengaduan,
idCategory: idCategoryFix,
idWarga: idWargaFix,
location,
image,
location: lokasi,
image: imageFix,
noPengaduan,
},
select: {
@@ -172,7 +173,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({
@@ -182,20 +183,77 @@ const PengaduanRoute = new Elysia({
}
})
return { success: true, message: 'pengaduan sudah dibuat' }
return { success: true, message: 'pengaduan sudah dibuat dengan nomer ' + noPengaduan + ', nomer ini akan digunakan untuk mengakses pengaduan ini' }
}, {
body: t.Object({
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(),
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" }),
judulPengaduan: t.String({
minLength: 3,
error: "Judul pengaduan harus diisi dan minimal 3 karakter",
examples: ["Sampah menumpuk di depan rumah"],
description: "Judul singkat dari pengaduan warga"
}),
detailPengaduan: t.String({
minLength: 5,
error: "Deskripsi pengaduan harus diisi dan minimal 10 karakter",
examples: ["Terdapat sampah yang menumpuk selama seminggu di depan rumah saya"],
description: "Penjelasan lebih detail mengenai pengaduan"
}),
lokasi: t.String({
minLength: 5,
error: "Lokasi pengaduan harus diisi",
examples: ["Jl. Raya No. 1, RT 01 RW 02, Darmasaba"],
description: "Alamat atau titik lokasi pengaduan"
}),
namaGambar: t.String({
optional: true,
examples: ["sampah.jpg"],
description: "Nama file gambar yang telah diupload (opsional)"
}),
kategoriId: t.String({
minLength: 1,
error: "ID kategori pengaduan harus diisi",
examples: ["kebersihan"],
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
}),
wargaId: t.String({
minLength: 1,
error: "ID warga harus diisi",
examples: ["budiman"],
description: "ID unik warga yang melapor (jika sudah terdaftar)"
}),
noTelepon: t.String({
minLength: 1,
error: "Nomor telepon harus diisi",
examples: ["08123456789", "+628123456789"],
description: "Nomor telepon warga pelapor"
}),
}),
detail: {
summary: "Create Pengaduan Warga",
description: `tool untuk membuat pengaduan warga`,
summary: "Buat Pengaduan Warga",
description: `
Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga.
Alur proses:
1. Sistem memvalidasi kategori pengaduan berdasarkan ID.
- Jika ID kategori tidak ditemukan, sistem akan mencari berdasarkan nama kategori.
- Jika tetap tidak ditemukan, kategori akan diset menjadi "lainnya".
2. Sistem memvalidasi data warga berdasarkan ID.
- Jika warga tidak ditemukan, sistem akan mencari berdasarkan nomor telepon.
- Jika tetap tidak ditemukan, data warga baru akan dibuat secara otomatis.
3. Sistem menghasilkan nomor pengaduan unik (noPengaduan).
4. Data pengaduan akan disimpan ke database, termasuk judul, detail, lokasi, gambar (opsional), dan data warga.
5. Sistem juga membuat catatan riwayat awal pengaduan dengan deskripsi "Pengaduan dibuat".
Respon:
- success: true jika pengaduan berhasil dibuat.
- message: berisi pesan sukses dan nomor pengaduan yang dapat digunakan untuk melacak status pengaduan.`,
tags: ["mcp"]
}
})
@@ -255,6 +313,11 @@ const PengaduanRoute = new Elysia({
const data = await prisma.pengaduan.findUnique({
where: {
id,
OR: [
{
noPengaduan: id
}
]
},
select: {
id: true,
@@ -332,7 +395,7 @@ const PengaduanRoute = new Elysia({
}, {
detail: {
summary: "Detail Pengaduan Warga",
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan`,
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id atau nomer Pengaduan`,
tags: ["mcp"]
}
})
@@ -622,7 +685,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)