Compare commits

...

53 Commits

Author SHA1 Message Date
c797d1fc46 upd: api jenna ai
Deskripsi:
- detail post pengaduan
- detail post pengajuan surat

No Issues
2025-12-11 17:05:53 +08:00
91e5f6a77e upd: console data 2025-12-11 14:18:43 +08:00
3f567b57b2 upd: detail api
Deskripsi
- detail pengaduan by nomer pengaduan
- detail pengajuan surat by nomer pengajuan

No Issues
2025-12-11 14:16:31 +08:00
fdf7b0a13f upd: list pengaduan dan list pelayanan surat api jenna ai 2025-12-11 12:04:16 +08:00
d76a702d2d Merge pull request 'upd: api pelayanan' (#72) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/72
2025-12-10 17:25:04 +08:00
6bc6a9d357 upd: api pelayanan 2025-12-10 17:24:12 +08:00
dee32b8cfd Merge pull request 'upd: api pelayanan surat' (#71) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/71
2025-12-10 16:31:18 +08:00
ff0b0273bf upd: api pelayanan surat 2025-12-10 16:30:51 +08:00
f8dcffa9c5 Merge pull request 'amalia/10-des-25' (#70) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/70
2025-12-10 11:45:12 +08:00
20e3056e04 upd: list pengaduan 2025-12-10 11:44:22 +08:00
84c9f405d6 upd: api jenna 2025-12-10 11:40:35 +08:00
22597c0159 Merge pull request 'upd: api jenna' (#69) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/69
2025-12-10 11:34:01 +08:00
0f9af404e1 upd: api jenna 2025-12-10 11:33:05 +08:00
676edaa22b Merge pull request 'upd: api jenna' (#68) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/68
2025-12-10 11:10:25 +08:00
7f6f495eaa upd: api jenna 2025-12-10 11:09:00 +08:00
b5af41b07d Merge pull request 'amalia/09-des-25' (#67) from amalia/09-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/67
2025-12-09 17:17:31 +08:00
6428f5084e upd: api jenna ai 2025-12-09 17:16:24 +08:00
bfc292ec6c upd: api jenna ai 2025-12-09 17:15:08 +08:00
3b71976863 Merge pull request 'upd: api' (#66) from amalia/09-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/66
2025-12-09 16:18:48 +08:00
5680466c98 upd: api 2025-12-09 16:17:39 +08:00
270f3687a3 Merge pull request 'upd: jenna ai mcp' (#65) from amalia/09-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/65
2025-12-09 15:24:40 +08:00
f5cc45937c upd: jenna ai mcp 2025-12-09 15:23:21 +08:00
5b4164b151 Merge pull request 'upd: api pelayanan jenna ai' (#64) from amalia/09-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/64
2025-12-09 14:15:20 +08:00
225c58b346 upd: api pelayanan jenna ai 2025-12-09 14:14:04 +08:00
b8b3aed86e Merge pull request 'upd: api jenna ai' (#63) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/63
2025-12-08 16:31:00 +08:00
fc530399dd upd: api jenna ai 2025-12-08 16:28:37 +08:00
281e34ea69 Merge pull request 'upd' (#62) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/62
2025-12-08 14:51:18 +08:00
f928fc504f upd 2025-12-08 14:50:29 +08:00
4fb98d0480 Merge pull request 'upd: api' (#61) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/61
2025-12-08 14:32:54 +08:00
bfb33e2105 upd: api 2025-12-08 14:32:05 +08:00
2579714000 Merge pull request 'upd' (#60) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/60
2025-12-08 14:12:36 +08:00
d69189cf7d upd
:  api tambah pengaduan
2025-12-08 14:10:34 +08:00
20e24a03aa Merge pull request 'amalia/08-des-25' (#59) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/59
2025-12-08 11:50:41 +08:00
c256f4b729 upd: api jenna ai
Deskripsi:
- create pengaduan pake nama dan nomer hp dari header

No Issues
2025-12-08 11:49:24 +08:00
9430ad3728 upd: update data pengaduan dari wawrga 2025-12-08 11:37:16 +08:00
c6c3ba95f8 upd: update data pengaduan by jenna AI 2025-12-08 11:04:03 +08:00
bipproduction
3c58230c3a tambahan untuk handle true user 2025-12-08 10:16:27 +08:00
d22b4b973f Merge pull request 'upd: api jenna ai' (#58) from amalia/02-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/58
2025-12-02 17:27:35 +08:00
700fbe3bd7 upd: api jenna ai 2025-12-02 17:26:43 +08:00
9b7a61e134 Merge pull request 'upd: api jenna ai' (#57) from amalia/02-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/57
2025-12-02 14:40:07 +08:00
2d376663bb upd: api jenna ai
Deskripsi:
- create tambah data pengajuan pelayanan surat

No Issues
2025-12-02 14:37:59 +08:00
7c669f3494 Merge pull request 'fix: dashboard admin' (#56) from amalia/02-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/56
2025-12-02 12:01:42 +08:00
0ed9dc6ddd fix: dashboard admin
Deskripsi:
- list pengaduan
- list pengajuan surat

No Issues
2025-12-02 12:00:46 +08:00
b9984c6337 Merge pull request 'amalia/02-des-25' (#55) from amalia/02-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/55
2025-12-02 11:51:54 +08:00
48a7d43713 fix: list pengaduan dan list pelayanan surat
Deskripsi:
- fix date string

No Issues
2025-12-02 11:50:58 +08:00
6a52d10faa upd: dashboard admin
Deskripsi:
- home > ganti warna

No Issues
2025-12-02 11:40:30 +08:00
2b94684570 upd: dashboard admin
Deskripsi:
- pagination list pengajuan surat

NO Issues
2025-12-02 11:32:16 +08:00
cc7c8eb704 upd: dashboard admin
Deskripsi:
- pagination list pengaduan

No Issues
2025-12-02 11:22:05 +08:00
4996da4189 Merge pull request 'upd: test api' (#54) from amalia/01-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/54
2025-12-01 16:45:32 +08:00
5af9b720ca Merge pull request 'amalia/01-des-25' (#53) from amalia/01-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/53
2025-12-01 16:03:21 +08:00
1f95c7d7d8 Merge pull request 'amalia/01-des-25' (#52) from amalia/01-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/52
2025-12-01 15:25:25 +08:00
f52f5f87ca Merge pull request 'upd' (#51) from amalia/01-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/51
2025-12-01 15:21:00 +08:00
9c7c9d8595 Merge pull request 'upd: api test' (#50) from amalia/01-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/50
2025-12-01 15:14:53 +08:00
9 changed files with 662 additions and 209 deletions

View File

@@ -21,7 +21,7 @@ export default function DashboardCountData() {
label="Pengaduan Hari Ini"
value={String(data?.data?.pengaduan?.today)}
change={String(data?.data?.pengaduan?.kenaikan) + "%"}
color={(data?.data?.pengaduan?.kenaikan || 0) > 0 ? "teal" : "gray"}
color={"gray"}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
@@ -30,7 +30,7 @@ export default function DashboardCountData() {
label="Pengajuan Surat Hari Ini"
value={String(data?.data?.pelayanan?.today)}
change={String(data?.data?.pelayanan?.kenaikan) + "%"}
color={(data?.data?.pelayanan?.kenaikan || 0) > 0 ? "teal" : "gray"}
color="gray"
/>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>

View File

@@ -343,16 +343,19 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
Setujui
</Button>
</Group>
) : (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
>
Surat
</Button>
</Group>
)
) :
data?.status === "selesai" ?
(
<Group justify="center" grow>
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
>
Surat
</Button>
</Group>
)
: <></>
}
</Grid.Col>
</Grid>

View File

@@ -6,8 +6,10 @@ import {
Container,
Divider,
Flex,
Grid,
Group,
Input,
Pagination,
Stack,
Tabs,
Text,
@@ -113,22 +115,26 @@ type StatusKey =
function ListPelayananSurat({ status }: { status: StatusKey }) {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", async () => {
const res = await apiFetch.api.pelayanan.list.get({
const { data, mutate, isLoading } = useSwr("/", async () =>
apiFetch.api.pelayanan.list.get({
query: {
status,
search: value,
take: "",
page: "",
page: page.toString(),
},
});
})
);
useShallowEffect(() => {
setPage(1);
mutate();
}, [status, value]);
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
});
useShallowEffect(() => {
mutate();
}, [status, value]);
}, [page]);
useShallowEffect(() => {
@@ -155,26 +161,39 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Card>
);
const list = data || [];
const list = data?.data?.data || [];
const total = data?.data?.total || 0;
const totalPage = data?.data?.totalPages || 1;
const pageSize = data?.data?.pageSize || 10;
const pageNow = data?.data?.page || 1;
const toDate = (d: any) => new Date(d);
return (
<Stack gap="xl">
<Group grow>
<Input
value={value}
placeholder="Cari pengajuan..."
onChange={(event) => setValue(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue("")}
style={{ display: value ? undefined : "none" }}
/>
}
/>
</Group>
<Grid>
<Grid.Col span={9}>
<Input
value={value}
placeholder="Cari pengajuan..."
onChange={(event) => setValue(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue("")}
style={{ display: value ? undefined : "none" }}
/>
}
/>
</Grid.Col>
<Grid.Col span={3}>
<Group justify="flex-end">
<Text size="sm" c="gray.5">{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination total={totalPage} value={page} onChange={setPage} withPages={false} />
</Group>
</Grid.Col>
</Grid>
{Array.isArray(list) && list?.length === 0 ? (
<Flex justify="center" align="center" py={"xl"}>
<Stack gap={4} align="center">
@@ -214,7 +233,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
#{v.noPengajuan}
</Title>
<Text size="sm" c="dimmed">
{v.updatedAt}
{String(v.updatedAt)}
</Text>
</Group>
</Flex>
@@ -247,7 +266,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
Tanggal Ajuan
</Text>
</Group>
<Text size="md">{v.createdAt}</Text>
<Text size="md">{toDate(v.createdAt).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })}</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">

View File

@@ -70,7 +70,7 @@ export default function DetailPengaduanPage() {
);
}
function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => void }) {
function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: () => void }) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [openedPreview, setOpenedPreview] = useState(false);

View File

@@ -6,8 +6,10 @@ import {
Container,
Divider,
Flex,
Grid,
Group,
Input,
Pagination,
Stack,
Tabs,
Text,
@@ -124,22 +126,25 @@ function ListPengaduan({ status }: { status: StatusKey }) {
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", async () => {
const res = await apiFetch.api.pengaduan.list.get({
const { data, mutate, isLoading } = useSwr("/", async () =>
apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
page: page.toString(),
},
});
})
);
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
});
useShallowEffect(() => {
setPage(1);
mutate();
}, [status, value]);
useShallowEffect(() => {
mutate();
}, [status, value]);
}, [page]);
useShallowEffect(() => {
const unsubscribe = subscribe(state, () => mutate());
@@ -163,31 +168,41 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Card>
);
const list = data || [];
const list = data?.data?.data || [];
const total = data?.data?.total || 0;
const totalPage = data?.data?.totalPages || 1;
const pageSize = data?.data?.pageSize || 10;
const pageNow = data?.data?.page || 1;
const toDate = (d: any) => new Date(d);
return (
<Stack gap="xl">
<Group grow>
<Input
value={value}
placeholder="Cari pengaduan..."
onChange={(event) => setValue(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue("")}
style={{ display: value ? undefined : "none" }}
/>
}
/>
{/* <Group justify="flex-end">
<Text size="sm">Menampilkan {Number(data?.data?.length) * (page - 1) + 1} {Math.min(10, Number(data?.data?.length) * page)} dari {Number(data?.data?.length)}</Text>
<Pagination total={Number(data?.data?.length)} value={page} onChange={setPage} withPages={false} />
</Group> */}
</Group>
{list.length === 0 ? (
<Grid>
<Grid.Col span={9}>
<Input
value={value}
placeholder="Cari pengaduan..."
onChange={(event) => setValue(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValue("")}
style={{ display: value ? undefined : "none" }}
/>
}
/>
</Grid.Col>
<Grid.Col span={3}>
<Group justify="flex-end">
<Text size="sm" c="gray.5">{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination total={totalPage} value={page} onChange={setPage} withPages={false} />
</Group>
</Grid.Col>
</Grid>
{Array.isArray(list) && list.length === 0 ? (
<Flex justify="center" align="center" py={"xl"}>
<Stack gap={4} align="center">
<IconFileSad size={32} color="gray" />
@@ -224,7 +239,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
#{v.noPengaduan}
</Title>
<Text size="sm" c="dimmed">
{v.updatedAt}
{String(v.updatedAt)}
</Text>
</Group>
</Flex>
@@ -257,7 +272,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
Tanggal Aduan
</Text>
</Group>
<Text size="md">{v.createdAt}</Text>
<Text size="md">{toDate(v.createdAt).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })}</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">

View File

@@ -1,6 +1,6 @@
export function isValidPhone(number: string): boolean {
const clean = number.replace(/[\s.-]/g, ""); // hapus spasi, titik, strip
const regex = /^(?:\+628|08)(\d{7,12})$/;
const regex = /^(?:\+62|62|0)8\d{7,12}$/;
return regex.test(clean);
}

View File

@@ -128,7 +128,8 @@ function convertToMcpContent(payload: any) {
export async function executeTool(
tool: any,
args: Record<string, any> = {},
baseUrl: string
baseUrl: string,
xPayload: Record<string, any> = {}
) {
const x = tool["x-props"] || {};
const method = (x.method || "GET").toUpperCase();
@@ -247,6 +248,9 @@ export async function executeTool(
// Execute fetch
console.log(`[MCP] → ${method} ${url}`);
for(const [key, value] of Object.entries(xPayload)) {
opts.headers![key] = value;
}
const res = await fetch(url, opts);
const resContentType = (res.headers.get("content-type") || "").toLowerCase();
@@ -281,7 +285,7 @@ export async function executeTool(
/* -------------------------
JSON-RPC Handler
------------------------- */
async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCResponse> {
async function handleMCPRequestAsync(request: JSONRPCRequest, xPayload: Record<string, any>): Promise<JSONRPCResponse> {
const { id, method, params } = request;
const makeError = (code: number, message: string, data?: any): JSONRPCResponse => ({
@@ -331,7 +335,7 @@ async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCRe
const baseUrl = (params?.credentials?.baseUrl as string) || process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
const args = params?.arguments || {};
const result = await executeTool(tool, args, baseUrl);
const result = await executeTool(tool, args, baseUrl, xPayload);
// Extract the meaningful payload (prefer nested .data if present)
const raw = extractRaw(result.data);
@@ -365,7 +369,7 @@ async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCRe
Elysia App & Routes
------------------------- */
export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
.post("/mcp", async ({ request, set }) => {
.post("/mcp", async ({ request, set, headers }) => {
set.headers["Content-Type"] = "application/json";
set.headers["Access-Control-Allow-Origin"] = "*";
@@ -378,12 +382,17 @@ export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
}
}
const xPayload = {
['x-user']: headers['x-user'] || "",
['x-phone']: headers['x-phone'] || ""
}
try {
const body = await request.json();
// If batch array -> allSettled for resilience
if (Array.isArray(body)) {
const promises = body.map((req: JSONRPCRequest) => handleMCPRequestAsync(req));
const promises = body.map((req: JSONRPCRequest) => handleMCPRequestAsync(req, xPayload));
const settled = await Promise.allSettled(promises);
const responses = settled.map((s) =>
s.status === "fulfilled"
@@ -401,7 +410,7 @@ export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
return responses;
}
const single = await handleMCPRequestAsync(body as JSONRPCRequest);
const single = await handleMCPRequestAsync(body as JSONRPCRequest, xPayload);
return single;
} catch (err: any) {
set.status = 400;

View File

@@ -3,7 +3,7 @@ import type { StatusPengaduan } from "generated/prisma"
import { createSurat } from "../lib/create-surat"
import { getLastUpdated } from "../lib/get-last-updated"
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
import { normalizePhoneNumber } from "../lib/normalizePhone"
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
const PelayananRoute = new Elysia({
@@ -104,8 +104,9 @@ const PelayananRoute = new Elysia({
// --- PELAYANAN SURAT ---
.get("/", async ({ query }) => {
const { phone } = query
.get("/", async ({ query, headers }) => {
// const { phone } = query
const phone = headers['x-phone'] || ""
const data = await prisma.pelayananAjuan.findMany({
orderBy: {
createdAt: "asc"
@@ -115,13 +116,34 @@ const PelayananRoute = new Elysia({
Warga: {
phone
}
},
select: {
noPengajuan: true,
status: true,
createdAt: true,
CategoryPelayanan: {
select: {
name: true
}
}
}
})
return data
const dataFix = data.map((item) => {
return {
noPengajuan: item.noPengajuan,
status: item.status,
category: item.CategoryPelayanan.name,
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
}
})
return dataFix
}, {
query: t.Object({
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
}),
// query: t.Object({
// phone: t.String({ minLength: 1, error: "phone harus diisi" }),
// }),
detail: {
summary: "List Ajuan Pelayanan Surat by Phone",
description: `tool untuk mendapatkan list ajuan pelayanan surat`,
@@ -130,17 +152,9 @@ const PelayananRoute = new Elysia({
})
.get("/detail", async ({ query }) => {
const { id } = query
const data = await prisma.pelayananAjuan.findFirst({
where: {
OR: [
{
noPengajuan: id
},
{
id: id
}
]
id: id
},
select: {
id: true,
@@ -170,6 +184,17 @@ const PelayananRoute = new Elysia({
}
})
if (!data) {
const datafix = {
pengajuan: {},
history: [],
warga: {},
syaratDokumen: [],
dataText: [],
}
return datafix
}
const dataSurat = await prisma.suratPelayanan.findFirst({
where: {
idPengajuanLayanan: data?.id,
@@ -280,22 +305,25 @@ const PelayananRoute = new Elysia({
syaratDokumen: dataSyaratFix,
dataText: dataTextFix,
}
return datafix
}, {
query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
}),
detail: {
summary: "Detail Ajuan Pelayanan Surat",
description: `tool untuk mendapatkan detail ajuan pelayanan surat`,
tags: ["mcp"]
summary: "Detail Ajuan Pelayanan Surat by ID",
description: `tool untuk mendapatkan detail ajuan pelayanan surat berdasarkan id`,
}
})
.post("/create", async ({ body }) => {
const { kategoriId, wargaId, noTelepon, dataText, syaratDokumen } = body
.post("/create", async ({ body, headers }) => {
const { kategoriId, dataText, syaratDokumen } = body
const namaWarga = headers['x-user'] || ""
const noTelepon = headers['x-phone'] || ""
const noPengajuan = await generateNoPengajuanSurat()
let idCategoryFix = kategoriId
let idWargaFix = wargaId
let idWargaFix = ""
const category = await prisma.categoryPelayanan.findUnique({
where: {
id: kategoriId,
@@ -317,36 +345,28 @@ const PelayananRoute = new Elysia({
}
const warga = await prisma.warga.findUnique({
if (!isValidPhone(noTelepon)) {
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
}
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const dataWarga = await prisma.warga.upsert({
where: {
id: wargaId,
phone: nomorHP
},
create: {
name: namaWarga,
phone: nomorHP,
},
update: {
name: namaWarga,
},
select: {
id: true
}
})
if (!warga) {
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const cariWarga = await prisma.warga.findFirst({
where: {
phone: nomorHP,
}
})
if (!cariWarga) {
const wargaCreate = await prisma.warga.create({
data: {
name: wargaId,
phone: nomorHP,
},
select: {
id: true
}
})
idWargaFix = wargaCreate.id
} else {
idWargaFix = cariWarga.id
}
}
idWargaFix = dataWarga.id
const pengaduan = await prisma.pelayananAjuan.create({
data: {
@@ -384,6 +404,7 @@ const PelayananRoute = new Elysia({
})
}
await prisma.syaratDokumenPelayanan.createMany({
data: dataInsertSyaratDokumen,
})
@@ -400,40 +421,34 @@ const PelayananRoute = new Elysia({
}
})
return { success: true, message: 'pengajuan surat sudah dibuat' }
return { success: true, message: 'pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini' }
}, {
body: t.Object({
kategoriId: t.String({
minLength: 1,
description: "ID atau nama kategori pelayanan surat yang dipilih. Jika berupa nama, sistem akan mencocokkan secara otomatis.",
examples: ["skusaha"],
error: "ID kategori harus diisi"
}),
// namaWarga: t.String({
// description: "Nama warga",
// examples: ["Budi Santoso"],
// error: "Nama warga harus diisi"
// }),
wargaId: t.String({
minLength: 1,
description: "ID warga atau nama warga. Jika ID tidak ditemukan, sistem akan mencari berdasarkan nama.",
examples: ["Budi Santoso"],
error: "ID warga harus diisi"
}),
noTelepon: t.String({
minLength: 8,
description: "Nomor HP warga yang akan dinormalisasi. Jika data warga tidak ditemukan berdasarkan idWarga, pencarian dilakukan via nomor ini.",
examples: ["081234567890"],
error: "Nomor telepon harus diisi"
}),
// noTelepon: t.String({
// error: "Nomor telepon harus diisi",
// examples: ["08123456789", "+628123456789"],
// description: "Nomor telepon warga pelapor"
// }),
dataText: t.Array(
t.Object({
jenis: t.String({
minLength: 1,
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
error: "jenis harus diisi"
}),
value: t.String({
minLength: 1,
description: "Isi atau nilai dari jenis field terkait.",
examples: ["Budi Santoso", "Laki-laki", "Denpasar, 28 Februari 1990", "Indonesia", "Islam", "Belum menikah", "Jl. Mawar No. 10", "Karyawan Swasta", "usaha makanan", "Jl. Melati No. 21"],
error: "value harus diisi"
@@ -462,13 +477,11 @@ const PelayananRoute = new Elysia({
syaratDokumen: t.Array(
t.Object({
jenis: t.String({
minLength: 1,
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
examples: ["ktp", "kk", "surat_pengantar_rt"],
error: "jenis harus diisi"
}),
value: t.String({
minLength: 1,
description: "Nama file atau identifier file dokumen yang diupload.",
examples: ["ktp_budi.png", "kk_budi.png"],
error: "value harus diisi"
@@ -488,11 +501,176 @@ const PelayananRoute = new Elysia({
),
}),
detail: {
summary: "Create Pengajuan Pelayanan Surat",
summary: "Buat Pengajuan Pelayanan Surat",
description: `tool untuk membuat pengajuan pelayanan surat dengan syarat dokumen serta data text sesuai kategori pelayanan surat yang dipilih`,
tags: ["mcp"]
}
})
.post("/detail-data", async ({ body }) => {
const { nomerPengajuan } = body
const data = await prisma.pelayananAjuan.findFirst({
where: {
noPengajuan: nomerPengajuan
},
select: {
id: true,
noPengajuan: true,
status: true,
createdAt: true,
updatedAt: true,
CategoryPelayanan: {
select: {
name: true,
dataText: true,
syaratDokumen: true,
}
},
Warga: {
select: {
name: true,
phone: true,
_count: {
select: {
Pengaduan: true,
PelayananAjuan: true,
}
}
}
},
}
})
if (!data) {
return { success: false, message: "Data tidak ditemukan" }
}
const dataSurat = await prisma.suratPelayanan.findFirst({
where: {
idPengajuanLayanan: data?.id,
isActive: true
},
select: {
id: true,
idCategory: true,
}
})
const dataSyarat = await prisma.syaratDokumenPelayanan.findMany({
where: {
idPengajuanLayanan: data?.id,
isActive: true
},
select: {
id: true,
jenis: true,
value: true,
}
})
const dataText = await prisma.dataTextPelayanan.findMany({
where: {
idPengajuanLayanan: data?.id,
isActive: true
},
select: {
id: true,
value: true,
jenis: true,
}
})
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
name: string;
desc: string;
}[];
const dataSyaratFix = dataSyarat.map((item) => {
const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc
return {
id: item.id,
jenis: desc,
value: item.value,
}
})
const dataTextFix = dataText.map((item) => {
const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis)
return {
id: item.id,
jenis: item.jenis,
value: item.value,
}
})
const dataHistory = await prisma.historyPelayanan.findMany({
where: {
idPengajuanLayanan: data?.id,
},
select: {
id: true,
deskripsi: true,
status: true,
createdAt: true,
idUser: true,
User: {
select: {
name: true,
}
}
}
})
const dataHistoryFix = dataHistory.map((item) => {
return {
id: item.id,
deskripsi: item.deskripsi,
status: item.status,
createdAt: item.createdAt,
idUser: item.idUser,
nameUser: item.User?.name,
}
})
const warga = {
name: data?.Warga?.name,
phone: data?.Warga?.phone,
pengaduan: data?.Warga?._count.Pengaduan,
pelayanan: data?.Warga?._count.PelayananAjuan,
}
const dataPengajuan = {
id: data?.id,
noPengajuan: data?.noPengajuan,
category: data?.CategoryPelayanan.name,
status: data?.status,
createdAt: data?.createdAt,
updatedAt: data?.updatedAt,
idSurat: dataSurat?.id,
}
const datafix = {
pengajuan: dataPengajuan,
history: dataHistoryFix,
warga: warga,
syaratDokumen: dataSyaratFix,
dataText: dataTextFix,
}
return datafix
}, {
body: t.Object({
nomerPengajuan: t.String({
description: "Nomor pengajuan pelayanan surat yang ingin diakses.",
examples: ["PS-101225-001", "PS-101225-002"],
error: "Nomor pengajuan harus diisi"
})
}),
detail: {
summary: "Detail Pengajuan Pelayanan Surat By Nomor Pengajuan",
description: `tool untuk mendapatkan detail pengajuan pelayanan surat berdasarkan nomor pengajuan`,
tags: ["mcp"]
}
})
.post("/update-status", async ({ body }) => {
const { id, status, keterangan, idUser, noSurat } = body
let deskripsi = ""
@@ -581,6 +759,14 @@ const PelayananRoute = new Elysia({
mode: "insensitive"
},
},
},
{
Warga: {
name: {
contains: search ?? "",
mode: "insensitive"
},
},
}
]
}
@@ -592,6 +778,11 @@ const PelayananRoute = new Elysia({
}
}
const totalData = await prisma.pelayananAjuan.count({
where
});
const data = await prisma.pelayananAjuan.findMany({
skip,
take: !take ? 10 : Number(take),
@@ -625,12 +816,20 @@ const PelayananRoute = new Elysia({
category: item.CategoryPelayanan.name,
warga: item.Warga.name,
status: item.status,
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
createdAt: item.createdAt.toISOString(),
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
}
})
return dataFix
const dataReturn = {
data: dataFix,
total: totalData,
page: Number(page) || 1,
pageSize: !take ? 10 : Number(take),
totalPages: Math.ceil(totalData / (!take ? 10 : Number(take)))
}
return dataReturn
}, {
query: t.Object({
take: t.String({ optional: true }),

View File

@@ -107,8 +107,10 @@ const PengaduanRoute = new Elysia({
// --- PENGADUAN ---
.post("/create", async ({ body }) => {
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, namaWarga, noTelepon } = body
.post("/create", async ({ body, headers }) => {
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId } = body
const namaWarga = headers['x-user'] || ""
const noTelepon = headers['x-phone'] || ""
let imageFix = namaGambar
const noPengaduan = await generateNoPengaduan()
let idCategoryFix = kategoriId
@@ -128,19 +130,21 @@ const PengaduanRoute = new Elysia({
}
})
if (!cariCategory) {
idCategoryFix = "lainnya"
} else {
idCategoryFix = cariCategory.id
}
}
} else {
idCategoryFix = "lainnya"
}
if (!isValidPhone(noTelepon)) {
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
return { success: false, message: `nomor telepon ${noTelepon} tidak valid, harap masukkan nomor yang benar` }
}
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
@@ -216,20 +220,20 @@ const PengaduanRoute = new Elysia({
})),
kategoriId: t.Optional(t.String({
examples: ["kebersihan"],
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
examples: ["kebersihan", "infrastruktur", "keamanan"],
description: "Nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
})),
namaWarga: t.String({
examples: ["budiman"],
description: "Nama warga yang melapor"
}),
// namaWarga: t.String({
// examples: ["budiman"],
// description: "Nama warga yang melapor"
// }),
noTelepon: t.String({
error: "Nomor telepon harus diisi",
examples: ["08123456789", "+628123456789"],
description: "Nomor telepon warga pelapor"
}),
// noTelepon: t.String({
// error: "Nomor telepon harus diisi",
// examples: ["08123456789", "+628123456789"],
// description: "Nomor telepon warga pelapor"
// }),
}),
detail: {
@@ -289,18 +293,90 @@ const PengaduanRoute = new Elysia({
description: `tool untuk update status pengaduan`
}
})
.post("/update", async ({ body }) => {
const { noPengaduan, judul, detail, lokasi, namaGambar } = body
let dataUpdate = {}
const cek = await prisma.pengaduan.findFirst({
where: {
noPengaduan,
},
select: {
id: true
}
})
if (!cek) {
return { success: false, message: 'gagal update status pengaduan, nomer ' + noPengaduan + ' tidak ditemukan' }
}
if (judul) {
dataUpdate = { title: judul }
}
if (detail) {
dataUpdate = { ...dataUpdate, detail }
}
if (lokasi) {
dataUpdate = { ...dataUpdate, location: lokasi }
}
if (namaGambar) {
dataUpdate = { ...dataUpdate, image: namaGambar }
}
const pengaduan = await prisma.pengaduan.updateMany({
where: {
noPengaduan
},
data: dataUpdate
})
const keys = Object.keys(dataUpdate).join(", ");
await prisma.historyPengaduan.create({
data: {
idPengaduan: cek.id,
deskripsi: `Pengaduan diupdate oleh warga (data yg diupdate: ${keys})`,
}
})
return { success: true, message: 'pengaduan dengan nomer ' + noPengaduan + ' sudah diupdate' }
}, {
body: t.Object({
noPengaduan: t.String({
error: "nomer pengaduan harus diisi",
description: "Nomer pengaduan yang ingin diupdate"
}),
judul: t.Optional(t.String({
error: "judul harus diisi",
description: "Judul pengaduan yang ingin diupdate"
})),
detail: t.Optional(t.String({
description: "detail pengaduan yang ingin diupdate"
})),
lokasi: t.Optional(t.String({
description: "lokasi pengaduan yang ingin diupdate"
})),
namaGambar: t.Optional(t.String({
description: "Nama file gambar yang telah diupload untuk update data pengaduan"
})),
}),
detail: {
summary: "Update Data Pengaduan",
description: `tool untuk update data pengaduan`,
tags: ["mcp"]
}
})
.get("/detail", async ({ query }) => {
const { id } = query
const data = await prisma.pengaduan.findFirst({
where: {
OR: [
{
noPengaduan: id
}, {
id: id
}
]
id: id
},
select: {
id: true,
@@ -335,6 +411,16 @@ const PengaduanRoute = new Elysia({
}
})
if (!data) {
const datafix = {
pengaduan: {},
history: [],
warga: {},
}
return datafix
}
const dataHistory = await prisma.historyPengaduan.findMany({
where: {
idPengaduan: data?.id,
@@ -389,50 +475,51 @@ const PengaduanRoute = new Elysia({
}
return datafix
}, {
detail: {
summary: "Detail Pengaduan Warga",
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id atau nomer Pengaduan`,
tags: ["mcp"]
summary: "Detail Pengaduan Warga By ID",
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id pengaduan`,
}
})
.get("/", async ({ query }) => {
const { take, page, search, phone } = query
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
.get("/", async ({ query, headers }) => {
// const { take, page, search } = query
const phone = headers['x-phone'] || ""
// const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
const data = await prisma.pengaduan.findMany({
skip,
take: !take ? 10 : Number(take),
// skip,
// take: !take ? 10 : Number(take),
orderBy: {
createdAt: "asc"
},
where: {
isActive: true,
OR: [
{
title: {
contains: search ?? "",
mode: "insensitive"
},
},
{
noPengaduan: {
contains: search ?? "",
mode: "insensitive"
},
},
{
detail: {
contains: search ?? "",
mode: "insensitive"
},
}
],
AND: {
Warga: {
phone: phone
}
}
// OR: [
// {
// title: {
// contains: search ?? "",
// mode: "insensitive"
// },
// },
// {
// noPengaduan: {
// contains: search ?? "",
// mode: "insensitive"
// },
// },
// {
// detail: {
// contains: search ?? "",
// mode: "insensitive"
// },
// }
// ],
// AND: {
// Warga: {
// phone: phone
// }
// }
},
select: {
id: true,
@@ -467,12 +554,11 @@ const PengaduanRoute = new Elysia({
return dataFix
}, {
query: t.Object({
take: t.String({ optional: true }),
page: t.String({ optional: true }),
search: t.String({ optional: true }),
phone: t.String({ minLength: 11, error: "phone harus diisi" }),
}),
// query: t.Object({
// take: t.String({ optional: true }),
// page: t.String({ optional: true }),
// search: t.String({ optional: true }),
// }),
detail: {
summary: "List Pengaduan Warga By Phone",
description: `tool untuk mendapatkan list pengaduan warga by phone`,
@@ -646,6 +732,10 @@ const PengaduanRoute = new Elysia({
}
}
const totalData = await prisma.pengaduan.count({
where
});
const data = await prisma.pengaduan.findMany({
skip,
take: !take ? 10 : Number(take),
@@ -683,12 +773,20 @@ const PengaduanRoute = new Elysia({
detail: item.detail,
status: item.status,
location: item.location,
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
createdAt: item.createdAt.toISOString(),
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
}
})
return dataFix
const dataReturn = {
data: dataFix,
total: totalData,
page: Number(page) || 1,
pageSize: !take ? 10 : Number(take),
totalPages: Math.ceil(totalData / (!take ? 10 : Number(take)))
}
return dataReturn
}, {
query: t.Object({
take: t.String({ optional: true }),
@@ -790,8 +888,118 @@ const PengaduanRoute = new Elysia({
description: "Tool untuk delete file Seafile",
},
})
.post("/detail-data", async ({ body }) => {
const { nomerPengaduan } = body
const data = await prisma.pengaduan.findFirst({
where: {
noPengaduan: nomerPengaduan
},
select: {
id: true,
noPengaduan: true,
title: true,
detail: true,
location: true,
image: true,
idCategory: true,
idWarga: true,
status: true,
keterangan: true,
createdAt: true,
updatedAt: true,
CategoryPengaduan: {
select: {
name: true
}
},
Warga: {
select: {
name: true,
phone: true,
_count: {
select: {
Pengaduan: true,
PelayananAjuan: true,
}
}
}
}
}
})
if (!data) {
return { success: false, message: "Data tidak ditemukan" };
}
const dataHistory = await prisma.historyPengaduan.findMany({
where: {
idPengaduan: data?.id,
},
select: {
id: true,
deskripsi: true,
status: true,
createdAt: true,
idUser: true,
User: {
select: {
name: true,
}
}
}
})
const dataHistoryFix = dataHistory.map((item: any) => ({
..._.omit(item, ["User", "createdAt"]),
nameUser: item.User?.name,
createdAt: item.createdAt
}))
const warga = {
name: data?.Warga?.name,
phone: data?.Warga?.phone,
pengaduan: data?.Warga?._count.Pengaduan,
pelayanan: data?.Warga?._count.PelayananAjuan,
}
const dataPengaduan = {
id: data?.id,
noPengaduan: data?.noPengaduan,
title: data?.title,
detail: data?.detail,
location: data?.location,
image: data?.image,
category: data?.CategoryPengaduan.name,
status: data?.status,
keterangan: data?.keterangan,
createdAt: data?.createdAt,
updatedAt: data?.updatedAt,
}
const datafix = {
pengaduan: dataPengaduan,
history: dataHistoryFix,
warga: warga,
}
return datafix
}, {
body: t.Object({
nomerPengaduan: t.String({
description: "Nomer pengaduan yg ingin diakses",
examples: ["PGD-101225-001", "PGD-101225-002"],
error: "Nomer pengaduan harus diisi",
}),
}),
detail: {
summary: "Detail Pengaduan Warga By Nomor Pengaduan",
description: `tool untuk mendapatkan detail data pengaduan berdasarkan nomor pengaduan`,
tags: ["mcp"]
}
})
;
export default PengaduanRoute