amalia/11-nov-25 #19

Merged
amaliadwiy merged 5 commits from amalia/11-nov-25 into main 2025-11-11 17:49:01 +08:00
12 changed files with 1237 additions and 331 deletions

View File

@@ -18,10 +18,14 @@ import Home from "./pages/Home";
import CredentialPage from "./pages/scr/dashboard/credential/credential_page"; import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
import DashboardHome from "./pages/scr/dashboard/dashboard_home"; import DashboardHome from "./pages/scr/dashboard/dashboard_home";
import ListPelayananPage from "./pages/scr/dashboard/pelayanan-surat/list_pelayanan_page"; import ListPelayananPage from "./pages/scr/dashboard/pelayanan-surat/list_pelayanan_page";
import DetailPelayananPage from "./pages/scr/dashboard/pelayanan-surat/detail_pelayanan_page";
import DetailWargaPage from "./pages/scr/dashboard/warga/detail_warga_page";
import ListWargaPage from "./pages/scr/dashboard/warga/list_warga_page";
import ListPage from "./pages/scr/dashboard/pengaduan/list_page"; import ListPage from "./pages/scr/dashboard/pengaduan/list_page";
import DetailPage from "./pages/scr/dashboard/pengaduan/detail_page"; import DetailPage from "./pages/scr/dashboard/pengaduan/detail_page";
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page"; import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout"; import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
import DetailSettingPage from "./pages/scr/dashboard/setting/detail_setting_page";
import ScrLayout from "./pages/scr/scr_layout"; import ScrLayout from "./pages/scr/scr_layout";
import DirPage from "./pages/dir/dir_page"; import DirPage from "./pages/dir/dir_page";
import NotFound from "./pages/NotFound"; import NotFound from "./pages/NotFound";
@@ -99,6 +103,18 @@ export default function AppRoutes() {
path="/scr/dashboard/pelayanan-surat/list-pelayanan" path="/scr/dashboard/pelayanan-surat/list-pelayanan"
element={<ListPelayananPage />} element={<ListPelayananPage />}
/> />
<Route
path="/scr/dashboard/pelayanan-surat/detail-pelayanan"
element={<DetailPelayananPage />}
/>
<Route
path="/scr/dashboard/warga/detail-warga"
element={<DetailWargaPage />}
/>
<Route
path="/scr/dashboard/warga/list-warga"
element={<ListWargaPage />}
/>
<Route <Route
path="/scr/dashboard/pengaduan/list" path="/scr/dashboard/pengaduan/list"
element={<ListPage />} element={<ListPage />}
@@ -111,6 +127,10 @@ export default function AppRoutes() {
path="/scr/dashboard/apikey/apikey" path="/scr/dashboard/apikey/apikey"
element={<ApikeyPage />} element={<ApikeyPage />}
/> />
<Route
path="/scr/dashboard/setting/detail-setting"
element={<DetailSettingPage />}
/>
</Route> </Route>
</Route> </Route>
<Route path="/dir/dir" element={<DirPage />} /> <Route path="/dir/dir" element={<DirPage />} />

View File

@@ -20,9 +20,13 @@ const clientRoutes = {
"/scr/dashboard/credential/credential": "/scr/dashboard/credential/credential", "/scr/dashboard/credential/credential": "/scr/dashboard/credential/credential",
"/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home", "/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home",
"/scr/dashboard/pelayanan-surat/list-pelayanan": "/scr/dashboard/pelayanan-surat/list-pelayanan", "/scr/dashboard/pelayanan-surat/list-pelayanan": "/scr/dashboard/pelayanan-surat/list-pelayanan",
"/scr/dashboard/pelayanan-surat/detail-pelayanan": "/scr/dashboard/pelayanan-surat/detail-pelayanan",
"/scr/dashboard/warga/detail-warga": "/scr/dashboard/warga/detail-warga",
"/scr/dashboard/warga/list-warga": "/scr/dashboard/warga/list-warga",
"/scr/dashboard/pengaduan/list": "/scr/dashboard/pengaduan/list", "/scr/dashboard/pengaduan/list": "/scr/dashboard/pengaduan/list",
"/scr/dashboard/pengaduan/detail": "/scr/dashboard/pengaduan/detail", "/scr/dashboard/pengaduan/detail": "/scr/dashboard/pengaduan/detail",
"/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey", "/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey",
"/scr/dashboard/setting/detail-setting": "/scr/dashboard/setting/detail-setting",
"/dir/dir": "/dir/dir", "/dir/dir": "/dir/dir",
"/*": "/*" "/*": "/*"
} as const; } as const;

View File

@@ -236,13 +236,13 @@ function NavigationDashboard() {
description: "Manage pelayanan surat", description: "Manage pelayanan surat",
}, },
{ {
path: "/scr/dashboard/user", path: "/scr/dashboard/warga/list-warga",
icon: <IconUsersGroup size={20} />, icon: <IconUsersGroup size={20} />,
label: "User", label: "Warga",
description: "Manage user", description: "Manage warga",
}, },
{ {
path: "/scr/dashboard/setting", path: "/scr/dashboard/setting/detail-setting",
icon: <IconSettings size={20} />, icon: <IconSettings size={20} />,
label: "Setting", label: "Setting",
description: description:

View File

@@ -0,0 +1,370 @@
import apiFetch from "@/lib/apiFetch";
import {
Anchor,
Badge,
Button,
Card,
Container,
Divider,
Flex,
Grid,
Group,
Modal,
Stack,
Table,
Text,
Textarea,
Title,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCategory,
IconFileCertificate,
IconInfoTriangle,
IconMapPin,
IconMessageReport,
IconPhotoScan,
IconUser,
} from "@tabler/icons-react";
import { useState } from "react";
import { useLocation } from "react-router-dom";
import useSwr from "swr";
export default function DetailPelayananPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPelayanan />
<DetailDataHistori />
</Stack>
</Grid.Col>
<Grid.Col span={4}>
<DetailUserPelayanan />
</Grid.Col>
</Grid>
</Container>
);
}
function DetailDataPelayanan() {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
return (
<>
<Modal
opened={opened}
onClose={close}
title={"Konfirmasi"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
{catModal === "tolak" ? (
<>
<Text>
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
</Text>
<Textarea size="md" minRows={5} />
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" color="red" onClick={close}>
Tolak
</Button>
</Group>
</>
) : (
<>
<Text>Anda yakin ingin menerima pengaduan ini?</Text>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" color="green" onClick={close}>
Terima
</Button>
</Group>
</>
)}
</Stack>
</Modal>
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Group gap="xs">
<Title order={4} c="gray.2">
Pelayanan Surat
</Title>
<Title order={4} c="dimmed">
#PGf-2345-33
</Title>
</Group>
<Badge
size="xl"
variant="light"
radius="sm"
color={"yellow"}
style={{ textTransform: "none" }}
>
antrian
</Badge>
</Flex>
<Divider my={0} />
<Grid>
<Grid.Col span={6}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
<Text size="md">Judul</Text>
</Group>
<Text size="md" c={"white"}>
Judul Pelayanan Surat
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconMapPin size={20} />
<Text size="md">Lokasi</Text>
</Group>
<Text size="md" c="white">
fwef
</Text>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={6}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconCategory size={20} />
<Text size="md">Kategori</Text>
</Group>
<Text size="md" c="white">
fwef
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconPhotoScan size={20} />
<Text size="md">Gambar</Text>
</Group>
<Anchor href="https://mantine.dev/" target="_blank">
Lihat Gambar
</Anchor>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={12}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
<Text size="md">Detail</Text>
</Group>
<Text size="md" c="white">
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
Illum, corporis iusto. Suscipit veritatis quas, non nobis
fuga, laudantium accusantium tempora sint aliquid architecto
totam esse eum excepturi nostrum fugiat ut.
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconInfoTriangle size={20} />
<Text size="md">Keterangan</Text>
</Group>
<Text size="md" c={"white"}>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At
fugiat eligendi nesciunt dolore? Maiores a cumque vitae
suscipit incidunt quos beatae modi, vel, id ullam quae
voluptas, deserunt quas placeat.
</Text>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={12}>
<Group justify="center" grow>
<Button
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
</Grid.Col>
</Grid>
</Stack>
</Card>
</>
);
}
function DetailDataHistori() {
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
return (
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengaduan
</Title>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Stack>
</Card>
);
}
function DetailUserPelayanan() {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
},
}),
);
useShallowEffect(() => {
mutate();
}, [status, value]);
const list = data?.data || [];
return (
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={4} c="gray.2">
Warga
</Title>
</Flex>
</Flex>
<Divider my={0} />
<Stack gap="md">
<Group justify="space-between">
<Group gap="xs">
<IconUser size={20} />
<Text size="md">Nama</Text>
</Group>
<Text size="md" c={"white"}>
Amalia Dwi Yustiani
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconMapPin size={20} />
<Text size="md">Telepon</Text>
</Group>
<Text size="md" c="white">
08123456789
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconMessageReport size={20} />
<Text size="md">Jumlah Pengaduan</Text>
</Group>
<Text size="md" c="white">
10
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconFileCertificate size={20} />
<Text size="md">Jumlah Pelayanan Surat</Text>
</Group>
<Text size="md" c="white">
10
</Text>
</Group>
</Stack>
</Stack>
</Card>
);
}

View File

@@ -133,6 +133,8 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
mutate(); mutate();
}, [status, value]); }, [status, value]);
const navigate = useNavigate();
if (isLoading) if (isLoading)
return ( return (
<Card <Card
@@ -169,17 +171,13 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
/> />
} }
/> />
{/* <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> </Group>
{list.length === 0 ? ( {list.length === 0 ? (
<Flex justify="center" align="center" py={"xl"}> <Flex justify="center" align="center" py={"xl"}>
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<IconFileSad size={32} color="gray" /> <IconFileSad size={32} color="gray" />
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
No pengaduan have been added yet. No pelayanan surat have been added yet.
</Text> </Text>
</Stack> </Stack>
</Flex> </Flex>
@@ -196,6 +194,11 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
borderColor: "rgba(100,100,100,0.2)", borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)", boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}} }}
onClick={() => {
navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${v.id}`,
);
}}
> >
<Stack gap="md"> <Stack gap="md">
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">

View File

@@ -2,18 +2,22 @@ import apiFetch from "@/lib/apiFetch";
import { import {
Anchor, Anchor,
Badge, Badge,
Button,
Card, Card,
Container, Container,
Divider, Divider,
Flex, Flex,
Grid, Grid,
Group, Group,
Image,
Modal,
Stack, Stack,
Table, Table,
Text, Text,
Title Textarea,
Title,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { import {
IconAlignJustified, IconAlignJustified,
IconCategory, IconCategory,
@@ -22,7 +26,7 @@ import {
IconMapPin, IconMapPin,
IconMessageReport, IconMessageReport,
IconPhotoScan, IconPhotoScan,
IconUser IconUser,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
@@ -51,7 +55,77 @@ export default function DetailPengaduanPage() {
} }
function DetailDataPengaduan() { function DetailDataPengaduan() {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [openedModalImage, { open: openModalImage, close: closeModalImage }] =
useDisclosure(false);
async function handleLihatGambar() {
const res = await apiFetch.api.pengaduan.image.get({
query: {
fileName: "57d5ce89-7d18-4244-9f4c-ca21b70adb7e",
},
});
console.error('client',res)
// const blob = await res.data?.blob();
// setImageSrc(URL.createObjectURL(blob!));
// openModalImage();
}
return ( return (
<>
<Modal
opened={opened}
onClose={close}
title={"Konfirmasi"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
{catModal === "tolak" ? (
<>
<Text>
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
</Text>
<Textarea size="md" minRows={5} />
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" color="red" onClick={close}>
Tolak
</Button>
</Group>
</>
) : (
<>
<Text>Anda yakin ingin menerima pengaduan ini?</Text>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" color="green" onClick={close}>
Terima
</Button>
</Group>
</>
)}
</Stack>
</Modal>
<Modal
opened={openedModalImage}
onClose={closeModalImage}
title="Gambar Pengaduan"
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Image src={imageSrc!} />
</Modal>
<Card <Card
radius="md" radius="md"
p="lg" p="lg"
@@ -90,20 +164,20 @@ function DetailDataPengaduan() {
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
<Group gap="xs"> <Group gap="xs">
<IconAlignJustified size={20} /> <IconAlignJustified size={20} />
<Text size="md"> <Text size="md">Judul</Text>
Judul
</Text>
</Group> </Group>
<Text size="md" c={"white"}>Judul Pengaduan</Text> <Text size="md" c={"white"}>
Judul Pengaduan
</Text>
</Flex> </Flex>
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
<Group gap="xs"> <Group gap="xs">
<IconMapPin size={20} /> <IconMapPin size={20} />
<Text size="md"> <Text size="md">Lokasi</Text>
Lokasi
</Text>
</Group> </Group>
<Text size="md" c="white">fwef</Text> <Text size="md" c="white">
fwef
</Text>
</Flex> </Flex>
</Stack> </Stack>
</Grid.Col> </Grid.Col>
@@ -112,20 +186,18 @@ function DetailDataPengaduan() {
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
<Group gap="xs"> <Group gap="xs">
<IconCategory size={20} /> <IconCategory size={20} />
<Text size="md"> <Text size="md">Kategori</Text>
Kategori
</Text>
</Group> </Group>
<Text size="md" c="white">fwef</Text> <Text size="md" c="white">
fwef
</Text>
</Flex> </Flex>
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
<Group gap="xs"> <Group gap="xs">
<IconPhotoScan size={20} /> <IconPhotoScan size={20} />
<Text size="md"> <Text size="md">Gambar</Text>
Gambar
</Text>
</Group> </Group>
<Anchor href="https://mantine.dev/" target="_blank"> <Anchor href="#" onClick={handleLihatGambar}>
Lihat Gambar Lihat Gambar
</Anchor> </Anchor>
</Flex> </Flex>
@@ -136,40 +208,65 @@ function DetailDataPengaduan() {
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
<Group gap="xs"> <Group gap="xs">
<IconAlignJustified size={20} /> <IconAlignJustified size={20} />
<Text size="md"> <Text size="md">Detail</Text>
Detail
</Text>
</Group> </Group>
<Text size="md" c="white"> <Text size="md" c="white">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Illum, corporis iusto. Suscipit veritatis quas, non nobis fuga, laudantium accusantium tempora sint aliquid architecto totam esse eum excepturi nostrum fugiat ut. Lorem ipsum dolor sit, amet consectetur adipisicing elit.
Illum, corporis iusto. Suscipit veritatis quas, non nobis
fuga, laudantium accusantium tempora sint aliquid architecto
totam esse eum excepturi nostrum fugiat ut.
</Text> </Text>
</Flex> </Flex>
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
<Group gap="xs"> <Group gap="xs">
<IconInfoTriangle size={20} /> <IconInfoTriangle size={20} />
<Text size="md"> <Text size="md">Keterangan</Text>
Keterangan
</Text>
</Group> </Group>
<Text size="md" c={"white"}> <Text size="md" c={"white"}>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At fugiat eligendi nesciunt dolore? Maiores a cumque vitae suscipit incidunt quos beatae modi, vel, id ullam quae voluptas, deserunt quas placeat. Lorem ipsum dolor, sit amet consectetur adipisicing elit. At
fugiat eligendi nesciunt dolore? Maiores a cumque vitae
suscipit incidunt quos beatae modi, vel, id ullam quae
voluptas, deserunt quas placeat.
</Text> </Text>
</Flex> </Flex>
</Stack> </Stack>
</Grid.Col> </Grid.Col>
<Grid.Col span={12}>
<Group justify="center" grow>
<Button
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
</Grid.Col>
</Grid> </Grid>
</Stack> </Stack>
</Card> </Card>
</>
); );
} }
function DetailDataHistori() { function DetailDataHistori() {
const elements = [ const elements = [
{ position: 6, mass: 12.011, symbol: 'C', name: 'Carbon' }, { position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen' }, { position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium' }, { position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium' }, { position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium' }, { position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
]; ];
const rows = elements.map((element) => ( const rows = elements.map((element) => (
@@ -212,8 +309,7 @@ function DetailDataHistori() {
</Table> </Table>
</Stack> </Stack>
</Card> </Card>
);
)
} }
function DetailUserPengaduan() { function DetailUserPengaduan() {
@@ -258,41 +354,41 @@ function DetailUserPengaduan() {
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
<Stack gap="md"> <Stack gap="md">
<Group justify="space-between" > <Group justify="space-between">
<Group gap="xs"> <Group gap="xs">
<IconUser size={20} /> <IconUser size={20} />
<Text size="md"> <Text size="md">Nama</Text>
Nama </Group>
<Text size="md" c={"white"}>
Amalia Dwi Yustiani
</Text> </Text>
</Group> </Group>
<Text size="md" c={"white"}>Amalia Dwi Yustiani</Text> <Group justify="space-between">
</Group>
<Group justify="space-between" >
<Group gap="xs"> <Group gap="xs">
<IconMapPin size={20} /> <IconMapPin size={20} />
<Text size="md"> <Text size="md">Telepon</Text>
Telepon </Group>
<Text size="md" c="white">
08123456789
</Text> </Text>
</Group> </Group>
<Text size="md" c="white">08123456789</Text> <Group justify="space-between">
</Group>
<Group justify="space-between" >
<Group gap="xs"> <Group gap="xs">
<IconMessageReport size={20} /> <IconMessageReport size={20} />
<Text size="md"> <Text size="md">Jumlah Pengaduan</Text>
Jumlah Pengaduan </Group>
<Text size="md" c="white">
10
</Text> </Text>
</Group> </Group>
<Text size="md" c="white">10</Text> <Group justify="space-between">
</Group>
<Group justify="space-between" >
<Group gap="xs"> <Group gap="xs">
<IconFileCertificate size={20} /> <IconFileCertificate size={20} />
<Text size="md"> <Text size="md">Jumlah Pelayanan Surat</Text>
Jumlah Pelayanan Surat
</Text>
</Group> </Group>
<Text size="md" c="white">10</Text> <Text size="md" c="white">
10
</Text>
</Group> </Group>
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -198,7 +198,9 @@ function ListPengaduan({ status }: { status: StatusKey }) {
borderColor: "rgba(100,100,100,0.2)", borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)", boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}} }}
onClick={() => navigate(`/scr/dashboard/pengaduan/detail?id=${v.id}`)} onClick={() =>
navigate(`/scr/dashboard/pengaduan/detail?id=${v.id}`)
}
> >
<Stack gap="md"> <Stack gap="md">
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">

View File

@@ -0,0 +1,150 @@
import { Button, Card, Container, Divider, Flex, Grid, Group, Input, NavLink, Stack, Table, Title } from "@mantine/core";
import { IconCircleOff, IconGauge, IconHome2 } from "@tabler/icons-react";
import { useLocation } from "react-router-dom";
export default function DetailSettingPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const type = query.get("type");
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={3}>
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<NavLink
href={`?type=profile`}
label="Profile"
leftSection={<IconHome2 size={16} stroke={1.5} />}
active={type === "profile" || !type}
/>
<NavLink
href={`?type=cat-pengaduan`}
label="Kategori Pengaduan"
leftSection={<IconGauge size={16} stroke={1.5} />}
active={type === "cat-pengaduan"}
/>
<NavLink
href={`?type=cat-pelayanan`}
label="Kategori Pelayanan Surat"
leftSection={<IconCircleOff size={16} stroke={1.5} />}
active={type === "cat-pelayanan"}
/>
<NavLink
href={`?type=desa`}
label="Desa"
leftSection={<IconCircleOff size={16} stroke={1.5} />}
active={type === "desa"}
/>
</Card>
</Grid.Col>
<Grid.Col span={9}>
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
{type === "cat-pengaduan"
? <KategoriPengaduanPage />
: type === "cat-pelayanan"
? <KategoriPengaduanPage />
: type === "desa"
? <KategoriPengaduanPage />
: <ProfilePage />}
</Card>
</Grid.Col>
</Grid>
</Container>
);
}
function ProfilePage() {
return (
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Profile Pengguna
</Title>
<Button variant="light">Edit</Button>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Group gap="xl" grow>
<Input.Wrapper label="Nama" description="" error="">
<Input value={"Amalia Dwi Yustiani"} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Phone" description="" error="">
<Input value={"08123456789"} readOnly />
</Input.Wrapper>
</Group>
<Group gap="xl" grow>
<Input.Wrapper label="Email" description="" error="">
<Input value={"amaliadwiyustiani@gmail.com"} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Role" description="" error="">
<Input value={"Admin"} readOnly />
</Input.Wrapper>
</Group>
</Stack>
</Stack>
)
}
function KategoriPengaduanPage() {
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
return (
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Kategori Pengaduan
</Title>
<Button variant="light">Tambah</Button>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Stack>
</Stack>
)
}

View File

@@ -0,0 +1,182 @@
import apiFetch from "@/lib/apiFetch";
import {
Avatar,
Box,
Card,
Container,
Divider,
Flex,
Grid,
Group,
Stack,
Table,
Text,
Title,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconMail, IconMapPin, IconPhone } from "@tabler/icons-react";
import { useState } from "react";
import { useLocation } from "react-router-dom";
import useSwr from "swr";
export default function DetailWargaPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={4}>
<DetailWarga />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataHistori />
<DetailDataHistori />
</Stack>
</Grid.Col>
</Grid>
</Container>
);
}
function DetailDataHistori() {
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
return (
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengaduan
</Title>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Stack>
</Card>
);
}
function DetailWarga() {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
},
}),
);
useShallowEffect(() => {
mutate();
}, [status, value]);
const list = data?.data || [];
return (
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Box
style={{
backgroundColor: "#f7d86c",
height: 100,
borderRadius: "12px",
position: "relative",
}}
/>
<Group>
{/* Profile image */}
<Avatar
src="https://i.pravatar.cc/150?img=32"
radius={100}
size={90}
style={{
position: "absolute",
top: 80,
left: 30,
border: "4px solid white",
}}
/>
{/* Main content */}
<Stack ml={115} gap={4}>
<Text fw={700} fz="lg">
Lizbeth Moore
</Text>
<Text fz="sm" c="dimmed">
Social Media Strategies
</Text>
</Stack>
</Group>
{/* Contact info */}
<Card radius="md" mt="md" p="md" withBorder={false}>
<Stack gap="xs">
<Group gap="xs">
<IconMail size={18} />
<Text size="sm">lizbeth.moore@email.com</Text>
</Group>
<Group gap="xs">
<IconPhone size={18} />
<Text size="sm">+1 555-7788</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} />
<Text size="sm">Greenway Ave, Los Angeles, CA, USA</Text>
</Group>
</Stack>
</Card>
</Card>
);
}

View File

@@ -0,0 +1,97 @@
import {
Button,
Card,
CloseButton,
Container,
Divider,
Flex,
Input,
Stack,
Table,
Title,
} from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
export default function ListWargaPage() {
const navigate = useNavigate();
const [value, setValue] = useState("");
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
<Table.Td>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${element.position}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
));
return (
<Container size="xl" py="xl" w={"100%"}>
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={3} c="gray.2">
List Data Warga
</Title>
<Input
value={value}
placeholder="Cari warga..."
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" }}
/>
}
/>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Stack>
</Card>
</Container>
);
}

View File

@@ -226,48 +226,3 @@ export async function getFileLink(config: Config, fileName: string): Promise<str
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`); const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`);
return `🔗 Link for ${fileName}:\n${(await downloadUrlResponse.text()).replace(/"/g, '')}` return `🔗 Link for ${fileName}:\n${(await downloadUrlResponse.text()).replace(/"/g, '')}`
} }
// function showHelp(): void {
// return `note - simple CLI for wibu not
// Usage:
// not3 ls List files
// not3 cat <file> Show file content
// not3 cp <local> [remote] Upload file
// not3 rm <remote> Remove file
// not3 mv <old> <new> Rename/move file
// not3 get <remote> [local] Download file
// not3 link <file> Get file link/URL
// not3 test Test API connection
// not3 config Edit config (~/.note.conf)
// Config (~/.note.conf):
// TOKEN=your_seafile_token
// REPO=repos/<repo-id>
// URL=your_seafile_url/api2
// Version: ${version}`);
// }
// --- Main ---
// async function not3(): Promise<void> {
// const [cmd, ...args] = process.argv.slice(2);
// if (cmd === 'config') return editConfig();
// const config = await loadConfig();
// switch (cmd) {
// case 'test': return testConnection(config);
// case 'ls': return listFiles(config);
// case 'cat': return args[0] ? catFile(config, args[0]) : console.error('Usage: bun note.ts cat <file>');
// case 'cp': return args[0] ? uploadFile(config, args[0], args[1]) : console.error('Usage: bun note.ts cp <local_file> [remote_file]');
// case 'rm': return args[0] ? removeFile(config, args[0]) : console.error('Usage: bun note.ts rm <remote_file>');
// case 'mv': return args[1] ? moveFile(config, args[0]!, args[1]) : console.error('Usage: bun note.ts mv <old_name> <new_name>');
// case 'get': return args[0] ? downloadFile(config, args[0], args[1]) : console.error('Usage: bun note.ts get <remote_file> [local_file]');
// case 'link': return args[0] ? getFileLink(config, args[0]) : console.error('Usage: bun note.ts link <file>');
// default: return showHelp();
// }
// }
// not3().catch((error) => {
// console.error('❌ Error:', error);
// process.exit(1);
// });

View File

@@ -6,7 +6,7 @@ import { mimeToExtension } from "../lib/mimetypeToExtension"
import { generateNoPengaduan } from "../lib/no-pengaduan" import { generateNoPengaduan } from "../lib/no-pengaduan"
import { normalizePhoneNumber } from "../lib/normalizePhone" import { normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma" import { prisma } from "../lib/prisma"
import { defaultConfigSF, uploadFile, uploadFileBase64 } from "../lib/seafile" import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64 } from "../lib/seafile"
const PengaduanRoute = new Elysia({ const PengaduanRoute = new Elysia({
prefix: "pengaduan", prefix: "pengaduan",
@@ -487,7 +487,6 @@ const PengaduanRoute = new Elysia({
consumes: ["multipart/form-data"] consumes: ["multipart/form-data"]
}, },
}) })
.get("/list", async ({ query }) => { .get("/list", async ({ query }) => {
const { take, page, search, status } = query const { take, page, search, status } = query
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take)) const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
@@ -619,6 +618,34 @@ const PengaduanRoute = new Elysia({
description: `tool untuk mendapatkan jumlah pengaduan warga`, description: `tool untuk mendapatkan jumlah pengaduan warga`,
} }
}) })
.get("/image", async ({ query, set }) => {
const { fileName } = query
const connect = await testConnection(defaultConfigSF)
console.log({connect})
const hasil = await catFile(defaultConfigSF, fileName)
console.log('hasilnya', hasil)
// Tentukan tipe MIME berdasarkan ekstensi
const ext = fileName.split(".").pop()?.toLowerCase();
const mime =
ext === "jpg" || ext === "jpeg"
? "image/jpeg"
: ext === "png"
? "image/png"
: "application/octet-stream";
set.headers["Content-Type"] = mime;
return new Response(hasil);
}, {
query: t.Object({
fileName: t.String(),
}),
detail: {
summary: "Gambar Pengaduan Warga",
description: `tool untuk mendapatkan gambar pengaduan warga`,
}
})
; ;
export default PengaduanRoute export default PengaduanRoute