Compare commits

...

12 Commits

Author SHA1 Message Date
01334ec573 upd: login
Deskripsi:
- update design login page

No Issues
2026-01-09 16:43:19 +08:00
98ad9b0d72 upd: loading saat melakukan aksi pada detail pengaduan
- mencegah 2x klik

NO Issues
2026-01-09 15:53:41 +08:00
c0471f47f3 upd: detail warga
Deskripsi:
- pagination pada list pengaduan dan list pengajuan surat
- search pada list pengaduan dan list pengajuan surat

No Issues
2026-01-09 15:46:30 +08:00
3d641d2035 Merge pull request 'upd: hapus console log' (#106) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/106
2026-01-08 17:38:19 +08:00
694115dbfb upd: hapus console log 2026-01-08 17:37:37 +08:00
7de5078868 Merge pull request 'upd: console log server2' (#105) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/105
2026-01-08 16:25:03 +08:00
7a3faa5719 upd: console log server2 2026-01-08 16:24:13 +08:00
ea5072d9ab Merge pull request 'upd: console log server' (#104) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/104
2026-01-08 16:03:03 +08:00
e8bb4f5a41 upd: console log server 2026-01-08 16:02:01 +08:00
d63bf024d3 Merge pull request 'upd: console log send wa' (#103) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/103
2026-01-08 15:50:37 +08:00
46f7dbf7bb upd: console log send wa 2026-01-08 15:49:51 +08:00
1adea29990 Merge pull request 'amalia/07-jan-26' (#102) from amalia/07-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/102
2026-01-07 17:11:50 +08:00
4 changed files with 323 additions and 87 deletions

View File

@@ -1,12 +1,12 @@
import clientRoutes from "@/clientRoutes"; import clientRoutes from "@/clientRoutes";
import { import {
Button, Button,
Container, Center,
Group, Paper,
PasswordInput, PasswordInput,
Stack, Stack,
Text, Text,
TextInput, TextInput
} from "@mantine/core"; } from "@mantine/core";
import { useState } from "react"; import { useState } from "react";
import apiFetch from "../lib/apiFetch"; import apiFetch from "../lib/apiFetch";
@@ -73,25 +73,73 @@ export default function Login() {
}; };
return ( return (
<Container> <Center
<Stack> h="100vh"
<Text>Login</Text> style={{
<TextInput background:
placeholder="Email" "radial-gradient(circle at top, #1f2d2b 0%, #0b0f0e 60%)",
value={email} }}
onChange={(e) => setEmail(e.target.value)} >
/> <Paper
<PasswordInput radius="lg"
placeholder="Password" p="xl"
value={password} w={420}
onChange={(e) => setPassword(e.target.value)} style={{
/> background: "rgba(20, 20, 20, 0.75)",
<Group justify="right"> backdropFilter: "blur(12px)",
<Button onClick={handleSubmit} disabled={loading}> border: "1px solid rgba(255, 255, 255, 0.08)",
boxShadow: "0 20px 60px rgba(0,0,0,0.6)",
}}
>
<Stack>
<Text
size="xl"
fw={700}
ta="center"
c="white"
>
Welcome Back
</Text>
<Text
size="sm"
ta="center"
c="dimmed"
>
Sign in to continue to your dashboard
</Text>
<TextInput
label="Email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<PasswordInput
label="Password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button
fullWidth
mt="md"
radius="md"
size="md"
variant="gradient"
gradient={{ from: "teal", to: "cyan", deg: 45 }}
style={{
transition: "all 0.2s ease",
}}
onClick={handleSubmit}
disabled={loading}
>
Login Login
</Button> </Button>
</Group> </Stack>
</Stack> </Paper>
</Container> </Center>
); );
} }

View File

@@ -93,6 +93,7 @@ function DetailDataPengaduan({
const [keterangan, setKeterangan] = useState(""); const [keterangan, setKeterangan] = useState("");
const [host, setHost] = useState<User | null>(null); const [host, setHost] = useState<User | null>(null);
const [permissions, setPermissions] = useState<JsonValue[]>([]); const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
async function fetchHost() { async function fetchHost() {
@@ -111,6 +112,7 @@ function DetailDataPengaduan({
const handleKonfirmasi = async (cat: "terima" | "tolak") => { const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try { try {
setIsLoading(true);
const res = await apiFetch.api.pengaduan["update-status"].post({ const res = await apiFetch.api.pengaduan["update-status"].post({
id: data?.id, id: data?.id,
status: status:
@@ -184,6 +186,8 @@ function DetailDataPengaduan({
message: "Failed to update pengaduan", message: "Failed to update pengaduan",
type: "error", type: "error",
}); });
} finally {
setIsLoading(false);
} }
}; };
@@ -218,6 +222,7 @@ function DetailDataPengaduan({
color="red" color="red"
disabled={keterangan.length < 1} disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")} onClick={() => handleKonfirmasi("tolak")}
loading={isLoading}
> >
Tolak Tolak
</Button> </Button>
@@ -242,6 +247,7 @@ function DetailDataPengaduan({
variant="filled" variant="filled"
color="green" color="green"
onClick={() => handleKonfirmasi("terima")} onClick={() => handleKonfirmasi("terima")}
loading={isLoading}
> >
Ya Ya
</Button> </Button>

View File

@@ -1,62 +1,66 @@
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { import {
Avatar, Avatar,
Box, Box,
Button, Button,
Card, Card,
CloseButton,
Container, Container,
Divider, Divider,
Flex, Flex,
Grid, Grid,
Group, Group,
Input,
LoadingOverlay, LoadingOverlay,
Pagination,
Stack, Stack,
Table, Table,
Text, Text,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { IconPhone } from "@tabler/icons-react"; import { IconPhone, IconSearch } from "@tabler/icons-react";
import _ from "lodash"; import _ from "lodash";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
export default function DetailWargaPage() { export default function DetailWargaPage() {
const { search } = useLocation(); const { search } = useLocation();
const query = new URLSearchParams(search); const query = new URLSearchParams(search);
const id = query.get("id"); const id = query.get("id");
const { data, mutate, isLoading } = useSwr("/", () => // const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.warga.detail.get({ // apiFetch.api.warga.detail.get({
query: { // query: {
id: id!, // id: id!,
}, // },
}), // }),
); // );
useShallowEffect(() => { // useShallowEffect(() => {
mutate(); // mutate();
}, []); // }, []);
return ( return (
<> <>
<LoadingOverlay <LoadingOverlay
visible={isLoading} // visible={isLoading}
zIndex={1000} zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }} overlayProps={{ radius: "sm", blur: 2 }}
/> />
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
<Grid> <Grid>
<Grid.Col span={4}> <Grid.Col span={4}>
<DetailWarga data={data?.data?.warga} /> <DetailWarga id={id!} />
</Grid.Col> </Grid.Col>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataHistori <DetailDataHistori
data={data?.data?.pengaduan} id={id!}
kategori="pengaduan" kategori="pengaduan"
/> />
<DetailDataHistori <DetailDataHistori
data={data?.data?.pelayanan} id={id!}
kategori="pelayanan" kategori="pelayanan"
/> />
</Stack> </Stack>
@@ -68,13 +72,66 @@ export default function DetailWargaPage() {
} }
function DetailDataHistori({ function DetailDataHistori({
data, id,
kategori, kategori,
}: { }: {
data: any; id: string;
kategori: "pengaduan" | "pelayanan"; kategori: "pengaduan" | "pelayanan";
}) { }) {
const navigate = useNavigate(); const navigate = useNavigate();
const [data, setData] = useState<any>([]);
const [totalPages, setTotalPages] = useState(1);
const [totalRows, setTotalRows] = useState(0);
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
async function getData() {
try {
const res = await apiFetch.api.warga.detail.get({
query: {
id,
category: kategori,
page: String(page),
search
}
}) as { data: { success: boolean; data: any[]; totalPages: number, totalRows: number } };
if (res?.data?.success) {
setData(res.data.data)
setTotalPages(res?.data?.totalPages)
setTotalRows(res?.data?.totalRows)
} else {
setData([])
setTotalPages(1)
setTotalRows(0)
notification({
title: "Failed",
message: "Failed to get data",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Failed",
message: "Failed to get data",
type: "error",
});
}
}
useShallowEffect(() => {
getData()
}, [page])
useShallowEffect(() => {
setPage(1)
if (page == 1) {
getData()
}
}, [search]);
return ( return (
<Card <Card
@@ -93,6 +150,36 @@ function DetailDataHistori({
<Title order={4} c="gray.2"> <Title order={4} c="gray.2">
Histori {_.upperFirst(kategori)} Histori {_.upperFirst(kategori)}
</Title> </Title>
<Flex
gap="md"
justify="flex-start"
align="center"
direction="row"
>
<Input
value={search}
placeholder="Cari data..."
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setSearch("")}
style={{ display: search ? undefined : "none" }}
/>
}
/>
<Text size="sm" c="gray.5" >
{`${5 * (page - 1) + 1} ${Math.min(totalRows, 5 * page)} of ${totalRows}`}
</Text>
<Pagination
total={totalPages}
value={page}
onChange={setPage}
withPages={false}
/>
</Flex>
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
<Table> <Table>
@@ -110,7 +197,7 @@ function DetailDataHistori({
{data?.length > 0 ? ( {data?.length > 0 ? (
data?.map((item: any, index: number) => ( data?.map((item: any, index: number) => (
<Table.Tr key={index}> <Table.Tr key={index}>
<Table.Td>{item.noPengaduan}</Table.Td> <Table.Td w={"180"}>{item.noPengaduan}</Table.Td>
<Table.Td> <Table.Td>
{kategori == "pengaduan" ? item.title : item.category} {kategori == "pengaduan" ? item.title : item.category}
</Table.Td> </Table.Td>
@@ -121,11 +208,11 @@ function DetailDataHistori({
onClick={() => { onClick={() => {
kategori == "pengaduan" kategori == "pengaduan"
? navigate( ? navigate(
`/scr/dashboard/pengaduan/detail?id=${item.id}`, `/scr/dashboard/pengaduan/detail?id=${item.id}`,
) )
: navigate( : navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`, `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
); );
}} }}
> >
Detail Detail
@@ -147,7 +234,33 @@ function DetailDataHistori({
); );
} }
function DetailWarga({ data }: { data: any }) { function DetailWarga({ id }: { id: string }) {
const [data, setData] = useState<any>(null);
async function getWarga() {
try {
const res = await apiFetch.api.warga.detail.get({
query: {
id: id,
category: "warga",
page: "1",
search: "",
},
});
setData(res.data);
} catch (error) {
console.error(error);
notification({
title: "Failed",
message: "Failed to get data warga",
type: "error",
});
}
}
useShallowEffect(() => {
getWarga();
}, []);
return ( return (
<Card <Card
radius="md" radius="md"

View File

@@ -97,68 +97,137 @@ const WargaRoute = new Elysia({
} }
}) })
.get("/detail", async ({ query }) => { .get("/detail", async ({ query }) => {
const { id } = query const { id, category, search, page } = query
const skip = !page ? 0 : (Number(page) - 1) * 5
const dataWarga = await prisma.warga.findUnique({ const dataWarga = await prisma.warga.findUnique({
where: { where: {
id id
} }
}) })
const dataPengaduan = await prisma.pengaduan.findMany({ if (!dataWarga)
orderBy: { return { success: false, message: "data warga tidak ditemukan", data: null, totalPages: 1, totalRows: 0 }
createdAt: "desc"
}, if (category == "warga") {
where: { return dataWarga
} else if (category == "pengaduan") {
const where: any = {
isActive: true, isActive: true,
idWarga: id idWarga: id,
}, OR: [
select: { {
id: true, title: {
status: true, contains: search ?? "",
noPengaduan: true, mode: "insensitive"
title: true },
},
{
noPengaduan: {
contains: search ?? "",
mode: "insensitive"
},
}
]
} }
})
const totalData = await prisma.pengaduan.count({
where
});
const dataPengaduan = await prisma.pengaduan.findMany({
skip,
take: 5,
orderBy: {
createdAt: "desc"
},
where,
select: {
id: true,
status: true,
noPengaduan: true,
title: true
}
})
const dataReturn = {
success: true,
message: "data pengaduan berhasil diambil",
data: dataPengaduan,
totalRows: totalData,
totalPages: Math.ceil(totalData / 5)
}
const dataPelayanan = await prisma.pelayananAjuan.findMany({ return dataReturn
orderBy: { } else if (category == "pelayanan") {
createdAt: "desc" const where: any = {
},
where: {
isActive: true, isActive: true,
idWarga: id idWarga: id,
}, OR: [
select: { {
id: true, CategoryPelayanan: {
noPengajuan: true, name: {
status: true, contains: search ?? "",
CategoryPelayanan: { mode: "insensitive"
select: { },
name: true },
},
{
noPengajuan: {
contains: search ?? "",
mode: "insensitive"
},
},
]
}
const totalData = await prisma.pelayananAjuan.count({
where
});
const dataPelayanan = await prisma.pelayananAjuan.findMany({
skip,
take: 5,
orderBy: {
createdAt: "desc"
},
where,
select: {
id: true,
noPengajuan: true,
status: true,
CategoryPelayanan: {
select: {
name: true
}
} }
} }
})
const dataPelayanFix = dataPelayanan.map((v: any) => ({
..._.omit(v, ["CategoryPelayanan"]),
id: v.id,
noPengaduan: v.noPengajuan,
status: v.status,
category: v.CategoryPelayanan.name
}))
const dataReturn = {
success: true,
message: "data pelayanan berhasil diambil",
data: dataPelayanFix,
totalRows: totalData,
totalPages: Math.ceil(totalData / 5)
} }
})
const dataPelayanFix = dataPelayanan.map((v: any) => ({ return dataReturn
..._.omit(v, ["CategoryPelayanan"]),
id: v.id,
noPengaduan: v.noPengajuan,
status: v.status,
category: v.CategoryPelayanan.name
}))
return {
warga: dataWarga,
pengaduan: dataPengaduan,
pelayanan: dataPelayanFix
} }
}, { }, {
query: t.Object({ query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }) id: t.String({ minLength: 1, error: "id harus diisi" }),
category: t.String({ minLength: 1, error: "kategori harus diisi" }),
page: t.String({ optional: true }),
search: t.String({ optional: true }),
}), }),
detail: { detail: {
summary: "Detail Warga", summary: "Detail Warga",