Compare commits

...

79 Commits

Author SHA1 Message Date
6fb6ab9750 upd: update data pengajuan surat 2025-12-15 15:45:29 +08:00
11a78d7371 upd: update edit pengajuan surat 2025-12-15 14:22:37 +08:00
3baba059ab upd: api update 2025-12-15 12:17:59 +08:00
dcd072034c upd: api update pelayanan pengajuan surat 2025-12-15 11:32:02 +08:00
ee27813da7 upd: update pengajuan surat 2025-12-12 17:32:22 +08:00
29f6ecfd23 upd: api update 2025-12-12 15:13:32 +08:00
d6882d4b3a upd: api update pelayanan surat 2025-12-12 15:08:09 +08:00
286c989bcf upd: api jenna ai
deskripsi:
- update pelayanan surat

No Issues:
2025-12-12 14:41:20 +08:00
031e408640 Merge pull request 'upd: api jenna ai' (#75) from amalia/11-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/75
2025-12-11 17:06:29 +08:00
c797d1fc46 upd: api jenna ai
Deskripsi:
- detail post pengaduan
- detail post pengajuan surat

No Issues
2025-12-11 17:05:53 +08:00
6f6905a414 Merge pull request 'amalia/11-des-25' (#74) from amalia/11-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/74
2025-12-11 14:19:34 +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
c98cfd21ce Merge pull request 'upd: list pengaduan dan list pelayanan surat api jenna ai' (#73) from amalia/11-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/73
2025-12-11 12:05:02 +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
c32cce838f upd: test api 2025-12-01 16:45:06 +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
35618bb438 upd: api test 2025-12-01 16:02:48 +08:00
eee8aadb1a upd: list warga 2025-12-01 16:02:13 +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
23df516aad upd: test 2025-12-01 15:24:59 +08:00
70175cedc6 upd: balik 2025-12-01 15:21:59 +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
ea17357638 upd 2025-12-01 15:20:29 +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
5ecf264155 upd: api test 2025-12-01 15:14:24 +08:00
4cc28c4311 Merge pull request 'upd dashboard admin' (#49) from amalia/01-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/49
2025-12-01 14:19:25 +08:00
c25e5eeba0 upd dashboard admin
Deskripsi:
- update api detail pengajuan surat
- table detail history jam pengajuan surat

No Issues
2025-12-01 14:18:23 +08:00
574603e290 Merge pull request 'upd: test update created At client' (#48) from amalia/01-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/48
2025-12-01 14:12:20 +08:00
ba76eb5e59 upd: test update created At client 2025-12-01 14:11:40 +08:00
6ae83ec19c Merge pull request 'fix: api tambah pengaduan jenna ai' (#47) from amalia/01-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/47
2025-12-01 11:22:36 +08:00
ba0414a99c fix: api tambah pengaduan jenna ai 2025-12-01 11:21:38 +08:00
4dd66dbd9a Merge pull request 'fix:list user' (#46) from amalia/28-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/46
2025-11-28 17:39:47 +08:00
dff1aa61c5 Merge pull request 'fixing : list user' (#45) from amalia/28-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/45
2025-11-28 17:33:29 +08:00
12 changed files with 1001 additions and 244 deletions

3
kirim.sh Normal file
View File

@@ -0,0 +1,3 @@
curl -X POST https://cld-dkr-prod-jenna-mcp.wibudev.com/api/pengaduan/upload-file-form-data \
-H "Accept: application/json" \
-F "file=@image.png"

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>
@@ -395,7 +398,17 @@ function DetailDataHistori({ data }: { data: any }) {
{
data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{
item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
})
}</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>

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);
@@ -394,7 +394,16 @@ function DetailDataHistori({ data }: { data: any }) {
{
data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>{
item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
})
}</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>

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

@@ -6,9 +6,12 @@ import {
Container,
Divider,
Flex,
Group,
Input,
Pagination,
Stack,
Table,
Text,
Title,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
@@ -19,21 +22,32 @@ import useSWR from "swr";
export default function ListWargaPage() {
const navigate = useNavigate();
const { data, mutate, isLoading } = useSWR("/", () =>
const [pages, setPages] = useState(1);
const [value, setValue] = useState("");
const { data, mutate } = useSWR("/", () =>
apiFetch.api.warga.list.get({
query: {
search: value,
page: pages,
},
}),
);
const list = data?.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 [value, setValue] = useState("");
useShallowEffect(() => {
setPages(1);
mutate();
}, [value]);
useShallowEffect(() => {
mutate();
}, [value]);
}, [pages]);
return (
@@ -48,10 +62,10 @@ export default function ListWargaPage() {
}}
>
<Stack gap="md">
<Title order={3} c="gray.2">
List Data Warga
</Title>
<Flex align="center" justify="space-between">
<Title order={3} c="gray.2">
List Data Warga
</Title>
<Input
value={value}
placeholder="Cari warga..."
@@ -66,6 +80,10 @@ export default function ListWargaPage() {
/>
}
/>
<Group>
<Text size="sm">{`${pageSize * (pages - 1) + 1} ${Math.min(total, pageSize * pages)} of ${total}`}</Text>
<Pagination total={totalPage} value={pages} onChange={setPages} withPages={false} />
</Group>
</Flex>
<Divider my={0} />
<Table>
@@ -86,8 +104,8 @@ export default function ListWargaPage() {
Array.isArray(list) && list?.map((item, i) => (
<Table.Tr key={i}>
<Table.Td>{item.name}</Table.Td>
<Table.Td>{item.phone}</Table.Td>
<Table.Td>
<Table.Td w={250}>{item.phone}</Table.Td>
<Table.Td w={150}>
<Button
variant="outline"
onClick={() => {

View File

@@ -1,6 +1,12 @@
export function isValidPhone(number: string): boolean {
const clean = number.replace(/[\s.-]/g, ""); // hapus spasi, titik, strip
const regex = /^(?:\+62|62|0)8\d{7,12}$/;
return regex.test(clean);
}
export function normalizePhoneNumber({ phone }: { phone: string }) {
// Hapus semua spasi, tanda hubung, atau karakter non-digit (+ tetap dipertahankan untuk dicek)
let cleaned = phone.trim().replace(/[\s-]/g, "");
let cleaned = phone.trim().replace(/[\s.-]/g, "");
// Jika diawali dengan +62 → ganti jadi 62
if (cleaned.startsWith("+62")) {

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,
@@ -250,14 +275,7 @@ const PelayananRoute = new Elysia({
id: item.id,
deskripsi: item.deskripsi,
status: item.status,
createdAt: item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
}),
createdAt: item.createdAt,
idUser: item.idUser,
nameUser: item.User?.name,
}
@@ -287,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,
@@ -324,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: {
@@ -391,6 +404,7 @@ const PelayananRoute = new Elysia({
})
}
await prisma.syaratDokumenPelayanan.createMany({
data: dataInsertSyaratDokumen,
})
@@ -407,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"
@@ -469,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"
@@ -495,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: item.jenis,
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 = ""
@@ -557,6 +728,199 @@ const PelayananRoute = new Elysia({
detail: {
summary: "Update Status Pengajuan Pelayanan Surat",
description: `tool untuk update status pengajuan pelayanan surat`,
}
})
.post("/update", async ({ body }) => {
const { nomerPengajuan, syaratDokumen, dataText } = body
let dataUpdate = []
console.log(body)
const pengajuan = await prisma.pelayananAjuan.findFirst({
where: {
noPengajuan: nomerPengajuan,
}
})
if (!pengajuan) {
console.log("data pengajuan surat tidak ditemukan")
return { success: false, message: 'data pengajuan surat tidak ditemukan' }
}
if (pengajuan.status != "ditolak" && pengajuan.status != "antrian") {
console.log("pengajuan surat tidak dapat diupdate karena status " + pengajuan.status)
return { success: false, message: 'pengajuan surat tidak dapat diupdate karena status ' + pengajuan.status }
}
if (dataText && dataText.length > 0) {
console.log("dataText")
for (const item of dataText) {
dataUpdate.push(item.jenis)
const hasil = await prisma.dataTextPelayanan.findFirst({
where: {
idPengajuanLayanan: pengajuan.id,
jenis: item.jenis,
}
})
const upd = await prisma.dataTextPelayanan.upsert({
where: {
id: hasil?.id
},
update: {
value: item.value,
},
create: {
value: item.value,
jenis: item.jenis,
idPengajuanLayanan: pengajuan.id,
idCategory: pengajuan.idCategory,
}
})
}
}
const category = await prisma.categoryPelayanan.findUnique({
where: {
id: pengajuan.idCategory,
}
})
type SyaratDokumen = {
desc: string;
name: string;
};
const syarat = category?.syaratDokumen as SyaratDokumen[] | undefined
if (syaratDokumen && syaratDokumen.length > 0) {
console.log("syaratDokumen")
for (const item of syaratDokumen) {
const pilih = syarat?.find((cat) => cat.desc == item.jenis || cat.name == item.jenis)?.name;
console.log(syarat, pilih)
dataUpdate.push(pilih)
const hasil = await prisma.syaratDokumenPelayanan.findFirst({
where: {
idPengajuanLayanan: pengajuan.id,
jenis: pilih,
}
})
console.log(hasil, item)
if (hasil && hasil.id) {
const upd = await prisma.syaratDokumenPelayanan.upsert({
where: {
id: hasil.id
},
update: {
value: item.value,
},
create: {
value: item.value,
jenis: hasil.jenis,
idPengajuanLayanan: pengajuan.id,
idCategory: pengajuan.idCategory,
}
})
} else {
return { success: false, message: 'dokumen tidak dapat diupload' }
}
}
}
const keys = dataUpdate.join(", ");
if (pengajuan.status == "ditolak") {
const updStatus = await prisma.pelayananAjuan.update({
where: {
id: pengajuan.id,
},
data: {
status: "antrian",
}
})
}
const history = await prisma.historyPelayanan.create({
data: {
idPengajuanLayanan: pengajuan.id,
deskripsi: `Pengajuan surat diupdate oleh warga (data yg diupdate: ${keys})`,
status: "antrian",
}
})
console.log("pengajuan surat sudah diperbarui")
return { success: true, message: 'pengajuan surat sudah diperbarui' }
}, {
body: t.Object({
nomerPengajuan: t.String({
error: "nomer pengajuan harus diisi",
description: "Nomer pengajuan yang ingin diupdate"
}),
dataText: t.Optional(t.Array(
t.Object({
jenis: t.String({
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({
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"
}),
}),
{
description: "Kumpulan data text dinamis sesuai kategori layanan.",
examples: [
[
{ jenis: "nama", value: "Budi Santoso" },
{ jenis: "jenis kelamin", value: "Laki-laki" },
{ jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ jenis: "negara", value: "Indonesia" },
{ jenis: "agama", value: "Islam" },
{ jenis: "status perkawinan", value: "Belum menikah" },
{ jenis: "alamat", value: "Jl. Mawar No. 10" },
{ jenis: "pekerjaan", value: "Karyawan Swasta" },
{ jenis: "jenis usaha", value: "usaha makanan" },
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" },
]
],
}
)),
syaratDokumen: t.Optional(t.Array(
t.Object({
jenis: t.String({
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
examples: ["ktp", "kk", "surat_pengantar_rt"],
error: "jenis harus diisi"
}),
value: t.String({
description: "Nama file atau identifier file dokumen yang diupload.",
examples: ["ktp_budi.png", "kk_budi.png"],
error: "value harus diisi"
}),
}),
{
description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.",
examples: [
[
{ jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
{ jenis: "ktp/kk", value: "kk_budi.png" },
{ jenis: "foto lokasi", value: "foto_lokasi_budi.png" }
]
],
}
)),
}),
detail: {
summary: "Update Data Pengajuan Pelayanan Surat",
description: `tool untuk update data pengajuan pelayanan surat`,
tags: ["mcp"]
}
})
@@ -588,6 +952,14 @@ const PelayananRoute = new Elysia({
mode: "insensitive"
},
},
},
{
Warga: {
name: {
contains: search ?? "",
mode: "insensitive"
},
},
}
]
}
@@ -599,6 +971,11 @@ const PelayananRoute = new Elysia({
}
}
const totalData = await prisma.pelayananAjuan.count({
where
});
const data = await prisma.pelayananAjuan.findMany({
skip,
take: !take ? 10 : Number(take),
@@ -632,12 +1009,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

@@ -5,10 +5,10 @@ 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 { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
import { renameFile } from "../lib/rename-file"
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileBase64 } from "../lib/seafile"
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile"
const PengaduanRoute = new Elysia({
prefix: "pengaduan",
@@ -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,17 +130,23 @@ const PengaduanRoute = new Elysia({
}
})
if (!cariCategory) {
idCategoryFix = "lainnya"
} else {
idCategoryFix = cariCategory.id
}
}
} else {
idCategoryFix = "lainnya"
}
if (!isValidPhone(noTelepon)) {
return { success: false, message: `nomor telepon ${noTelepon} tidak valid, harap masukkan nomor yang benar` }
}
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const dataWarga = await prisma.warga.upsert({
where: {
@@ -212,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.Optional(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: {
@@ -285,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,
@@ -331,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,
@@ -353,14 +443,7 @@ const PengaduanRoute = new Elysia({
const dataHistoryFix = dataHistory.map((item: any) => ({
..._.omit(item, ["User", "createdAt"]),
nameUser: item.User?.name,
createdAt: item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
}),
createdAt: item.createdAt
}))
@@ -392,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,
@@ -470,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`,
@@ -516,14 +599,57 @@ const PengaduanRoute = new Elysia({
detail: {
summary: "Upload File (FormData)",
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
tags: ["mcp"],
consumes: ["multipart/form-data"]
},
})
.post("/upload-file-form-data", async ({ body }) => {
const { file } = body;
// // Validasi file
// if (!file) {
// return { success: false, message: "File tidak ditemukan" };
// }
// // Rename file
// const renamedFile = renameFile({ oldFile: file, newName: 'random' });
// // Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
// // const buffer = await file.arrayBuffer();
// const result = await uploadFile(defaultConfigSF, renamedFile, 'pengaduan');
// if (result == 'gagal') {
// return { success: false, message: "Upload gagal" };
// }
return {
success: true,
file: JSON.stringify(file),
fileInfo: {
name: file.name || 'kosong',
size: file.size || 0,
type: file.type || 'kosong'
}
// message: "Upload berhasil",
// filename: renamedFile.name,
// size: renamedFile.size,
// seafileResult: result
};
}, {
body: t.Object({
file: t.Any(),
// folder: t.String(),
}),
detail: {
summary: "Upload File (FormData)",
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
consumes: ["multipart/form-data"]
},
})
.post("/upload-base64", async ({ body }) => {
const { data, mimetype } = body;
const { data, mimetype, kategori } = body;
const ext = mimeToExtension(mimetype)
const name = `${uuidv4()}.${ext}`
const kategoriFix = kategori === 'pengaduan' ? 'pengaduan' : 'syarat-dokumen';
// Validasi file
if (!data) {
@@ -535,7 +661,8 @@ const PengaduanRoute = new Elysia({
// const base64String = Buffer.from(buffer).toString("base64");
// (Opsional) jika perlu dikirim ke Seafile sebagai base64
const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
// const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
const result = await uploadFileToFolder(defaultConfigSF, { name: name, data: data }, kategoriFix);
return {
success: true,
@@ -544,17 +671,18 @@ const PengaduanRoute = new Elysia({
name,
mimetype,
ext,
kategori,
}
};
}, {
body: t.Object({
data: t.String(),
mimetype: t.String()
mimetype: t.String(),
kategori: t.String()
}),
detail: {
summary: "Upload File (Base64)",
description: "Tool untuk upload file ke Seafile dalam format Base64",
tags: ["mcp"],
consumes: ["multipart/form-data"]
},
})
@@ -601,6 +729,10 @@ const PengaduanRoute = new Elysia({
}
}
const totalData = await prisma.pengaduan.count({
where
});
const data = await prisma.pengaduan.findMany({
skip,
take: !take ? 10 : Number(take),
@@ -638,12 +770,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 }),
@@ -745,8 +885,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

View File

@@ -9,9 +9,31 @@ const WargaRoute = new Elysia({
})
.get("/list", async ({ query }) => {
const { search } = query
const { search, page = 1 } = query
const dataSkip = page == null || page == undefined ? 0 : Number(page) * 10 - 10;
const totalData = await prisma.warga.count({
where: {
OR: [
{
name: {
contains: search,
mode: "insensitive"
}
},
{
phone: {
contains: search,
mode: "insensitive"
}
}
]
}
});
const data = await prisma.warga.findMany({
skip: dataSkip,
take: 10,
where: {
OR: [
{
@@ -33,7 +55,15 @@ const WargaRoute = new Elysia({
}
})
return data
const dataFix = {
data,
total: totalData,
page: Number(page) || 1,
pageSize: 10,
totalPages: Math.ceil(totalData / 10)
};
return dataFix
}, {
detail: {
summary: "List Warga",