Compare commits
6 Commits
amalia/06-
...
amalia/07-
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c71d000f6 | |||
| 621cfc931a | |||
| 928ecb4c76 | |||
| e0456b2dba | |||
| 14ec81d98d | |||
| 0e5fab6a84 |
@@ -1,28 +1,29 @@
|
||||
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import DarmasabaHome from "./pages/darmasaba/darmasaba_home";
|
||||
import DarmasabaLayout from "./pages/darmasaba/darmasaba_layout";
|
||||
import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
|
||||
import FormKartuTandaPenduduk from "./pages/darmasaba/form_kartu_tanda_penduduk";
|
||||
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
|
||||
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
|
||||
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
|
||||
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
|
||||
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
|
||||
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
|
||||
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
|
||||
import FormSuratKeteranganTidakMampu from "./pages/darmasaba/form_surat_keterangan_tidak_mampu";
|
||||
import FormSuratKeteranganUsaha from "./pages/darmasaba/form_surat_keterangan_usaha";
|
||||
import DirPage from "./pages/dir/dir_page";
|
||||
import Home from "./pages/Home";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Login from "./pages/Login";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
|
||||
import DarmasabaLayout from "./pages/darmasaba/darmasaba_layout";
|
||||
import FormSuratKeteranganUsaha from "./pages/darmasaba/form_surat_keterangan_usaha";
|
||||
import FormSuratKeteranganTidakMampu from "./pages/darmasaba/form_surat_keterangan_tidak_mampu";
|
||||
import DarmasabaHome from "./pages/darmasaba/darmasaba_home";
|
||||
import FormKartuTandaPenduduk from "./pages/darmasaba/form_kartu_tanda_penduduk";
|
||||
import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
|
||||
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
|
||||
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
|
||||
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
|
||||
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
|
||||
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
|
||||
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
|
||||
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
|
||||
import Home from "./pages/Home";
|
||||
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
|
||||
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
|
||||
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
|
||||
import ListPelayananPage from "./pages/scr/dashboard/pelayanan-surat/list_pelayanan_page";
|
||||
import ListPage from "./pages/scr/dashboard/pengaduan/list_page";
|
||||
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
|
||||
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
|
||||
import ScrLayout from "./pages/scr/scr_layout";
|
||||
import DirPage from "./pages/dir/dir_page";
|
||||
import NotFound from "./pages/NotFound";
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
@@ -93,6 +94,10 @@ export default function AppRoutes() {
|
||||
path="/scr/dashboard/dashboard-home"
|
||||
element={<DashboardHome />}
|
||||
/>
|
||||
<Route
|
||||
path="/scr/dashboard/pelayanan-surat/list-pelayanan"
|
||||
element={<ListPelayananPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/scr/dashboard/pengaduan/list"
|
||||
element={<ListPage />}
|
||||
|
||||
@@ -19,6 +19,7 @@ const clientRoutes = {
|
||||
"/scr/dashboard": "/scr/dashboard",
|
||||
"/scr/dashboard/credential/credential": "/scr/dashboard/credential/credential",
|
||||
"/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home",
|
||||
"/scr/dashboard/pelayanan-surat/list-pelayanan": "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
||||
"/scr/dashboard/pengaduan/list": "/scr/dashboard/pengaduan/list",
|
||||
"/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey",
|
||||
"/dir/dir": "/dir/dir",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MCPRoute } from "./server/routes/mcp_route";
|
||||
import PelayananRoute from "./server/routes/pelayanan_surat_route";
|
||||
import PengaduanRoute from "./server/routes/pengaduan_route";
|
||||
import UserRoute from "./server/routes/user_route";
|
||||
import cors from "@elysiajs/cors";
|
||||
|
||||
const Docs = new Elysia({
|
||||
tags: ["docs"],
|
||||
@@ -40,6 +41,13 @@ const app = new Elysia()
|
||||
.use(Api)
|
||||
.use(Docs)
|
||||
.use(Auth)
|
||||
.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
methods: ["GET", "POST", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type"],
|
||||
}),
|
||||
)
|
||||
.get(
|
||||
"/.well-known/mcp.json",
|
||||
async () => {
|
||||
|
||||
@@ -230,7 +230,7 @@ function NavigationDashboard() {
|
||||
description: "Manage pengaduan warga",
|
||||
},
|
||||
{
|
||||
path: "/scr/dashboard/pelayanan",
|
||||
path: "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
||||
icon: <IconFileCertificate size={20} />,
|
||||
label: "Pelayanan Surat",
|
||||
description: "Manage pelayanan surat",
|
||||
|
||||
271
src/pages/scr/dashboard/pelayanan-surat/list_pelayanan_page.tsx
Normal file
271
src/pages/scr/dashboard/pelayanan-surat/list_pelayanan_page.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CloseButton,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconAlignJustified,
|
||||
IconClockHour3,
|
||||
IconFileSad,
|
||||
IconMapPin,
|
||||
IconSearch,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useSwr from "swr";
|
||||
import { proxy } from "valtio";
|
||||
|
||||
const state = proxy({ reload: "" });
|
||||
function reloadState() {
|
||||
state.reload = Math.random().toString();
|
||||
}
|
||||
|
||||
export default function PelayananSuratListPage() {
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const status = query.get("status") as StatusKey;
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Stack gap="xl">
|
||||
<TabListPelayananSurat status={status || "semua"} />
|
||||
<ListPelayananSurat status={status || "semua"} />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function TabListPelayananSurat({ status }: { status: string }) {
|
||||
const navigate = useNavigate();
|
||||
const dataCount = useSwr("/pelayanan-surat/count", () =>
|
||||
apiFetch.api.pengaduan.count.get().then((res) => res.data),
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue={status || "semua"} color="teal">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab
|
||||
value="all"
|
||||
onClick={() => {
|
||||
navigate("?status=semua");
|
||||
}}
|
||||
>
|
||||
Semua ({dataCount?.data?.semua || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="antrian"
|
||||
onClick={() => {
|
||||
navigate("?status=antrian");
|
||||
}}
|
||||
>
|
||||
Antrian ({dataCount?.data?.antrian || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="diterima"
|
||||
onClick={() => {
|
||||
navigate("?status=diterima");
|
||||
}}
|
||||
>
|
||||
Diterima ({dataCount?.data?.diterima || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="dikerjakan"
|
||||
onClick={() => {
|
||||
navigate("?status=dikerjakan");
|
||||
}}
|
||||
>
|
||||
Dikerjakan ({dataCount?.data?.dikerjakan || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="selesai"
|
||||
onClick={() => {
|
||||
navigate("?status=selesai");
|
||||
}}
|
||||
>
|
||||
Selesai ({dataCount?.data?.selesai || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="ditolak"
|
||||
onClick={() => {
|
||||
navigate("?status=ditolak");
|
||||
}}
|
||||
>
|
||||
Ditolak ({dataCount?.data?.ditolak || 0})
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
type StatusKey =
|
||||
| "antrian"
|
||||
| "diterima"
|
||||
| "dikerjakan"
|
||||
| "ditolak"
|
||||
| "selesai"
|
||||
| "semua";
|
||||
function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
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]);
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="xl"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c="dimmed">
|
||||
Loading pengaduan...
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const list = data?.data || [];
|
||||
|
||||
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 ? (
|
||||
<Flex justify="center" align="center" py={"xl"}>
|
||||
<Stack gap={4} align="center">
|
||||
<IconFileSad size={32} color="gray" />
|
||||
<Text c="dimmed" size="sm">
|
||||
No pengaduan have been added yet.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
) : (
|
||||
list.map((v: any) => (
|
||||
<Card
|
||||
key={v.id}
|
||||
radius="lg"
|
||||
p="xl"
|
||||
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={3} c="gray.2">
|
||||
{v.title}
|
||||
</Title>
|
||||
<Group>
|
||||
<Title order={6} c="gray.5">
|
||||
#{v.noPengaduan}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{v.updatedAt}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Badge
|
||||
size="xl"
|
||||
variant="light"
|
||||
radius="sm"
|
||||
color={
|
||||
v.status === "diterima"
|
||||
? "green"
|
||||
: v.status === "ditolak"
|
||||
? "red"
|
||||
: v.status === "selesai"
|
||||
? "blue"
|
||||
: v.status === "dikerjakan"
|
||||
? "purple"
|
||||
: "yellow"
|
||||
}
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
{v.status}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap="sm">
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconClockHour3 size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Tanggal Aduan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.createdAt}</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Lokasi
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.location}</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconAlignJustified size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Detail
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.detail}</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -2,21 +2,29 @@ import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CloseButton,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
Title
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { IconAlignJustified, IconClockHour3, IconMapPin } from "@tabler/icons-react";
|
||||
import {
|
||||
IconAlignJustified,
|
||||
IconClockHour3,
|
||||
IconFileSad,
|
||||
IconMapPin,
|
||||
IconSearch,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useSwr from "swr";
|
||||
import { proxy, subscribe } from "valtio";
|
||||
import { proxy } from "valtio";
|
||||
|
||||
const state = proxy({ reload: "" });
|
||||
function reloadState() {
|
||||
@@ -26,19 +34,13 @@ function reloadState() {
|
||||
export default function PengaduanListPage() {
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
|
||||
const status = query.get("status");
|
||||
console.log(status, "status");
|
||||
const status = query.get("status") as StatusKey;
|
||||
|
||||
return (
|
||||
<Container
|
||||
size="xl"
|
||||
py="xl"
|
||||
w={"100%"}
|
||||
>
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Stack gap="xl">
|
||||
<TabListPengaduan status={status || "all"} />
|
||||
<ListPengaduan />
|
||||
<TabListPengaduan status={status || "semua"} />
|
||||
<ListPengaduan status={status || "semua"} />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
@@ -46,47 +48,90 @@ export default function PengaduanListPage() {
|
||||
|
||||
function TabListPengaduan({ status }: { status: string }) {
|
||||
const navigate = useNavigate();
|
||||
const dataCount = useSwr("/pengaduan/count", () =>
|
||||
apiFetch.api.pengaduan.count.get().then((res) => res.data),
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue={status || "all"} color="teal">
|
||||
<Tabs defaultValue={status || "semua"} color="teal">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="all" onClick={() => { navigate("?status=all") }}>Semua</Tabs.Tab>
|
||||
<Tabs.Tab value="antrian" onClick={() => { navigate("?status=antrian") }}>Antrian</Tabs.Tab>
|
||||
<Tabs.Tab value="diterima" onClick={() => { navigate("?status=diterima") }}>Diterima</Tabs.Tab>
|
||||
<Tabs.Tab value="dikerjakan" onClick={() => { navigate("?status=dikerjakan") }}>Dikerjakan</Tabs.Tab>
|
||||
<Tabs.Tab value="ditolak" onClick={() => { navigate("?status=ditolak") }}>Ditolak</Tabs.Tab>
|
||||
<Tabs.Tab value="selesai" onClick={() => { navigate("?status=selesai") }}>Selesai</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="all"
|
||||
onClick={() => {
|
||||
navigate("?status=semua");
|
||||
}}
|
||||
>
|
||||
Semua ({dataCount?.data?.semua || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="antrian"
|
||||
onClick={() => {
|
||||
navigate("?status=antrian");
|
||||
}}
|
||||
>
|
||||
Antrian ({dataCount?.data?.antrian || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="diterima"
|
||||
onClick={() => {
|
||||
navigate("?status=diterima");
|
||||
}}
|
||||
>
|
||||
Diterima ({dataCount?.data?.diterima || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="dikerjakan"
|
||||
onClick={() => {
|
||||
navigate("?status=dikerjakan");
|
||||
}}
|
||||
>
|
||||
Dikerjakan ({dataCount?.data?.dikerjakan || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="selesai"
|
||||
onClick={() => {
|
||||
navigate("?status=selesai");
|
||||
}}
|
||||
>
|
||||
Selesai ({dataCount?.data?.selesai || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="ditolak"
|
||||
onClick={() => {
|
||||
navigate("?status=ditolak");
|
||||
}}
|
||||
>
|
||||
Ditolak ({dataCount?.data?.ditolak || 0})
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
function ListPengaduan() {
|
||||
type StatusKey =
|
||||
| "antrian"
|
||||
| "diterima"
|
||||
| "dikerjakan"
|
||||
| "ditolak"
|
||||
| "selesai"
|
||||
| "semua";
|
||||
function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
apiFetch.api.credential.list.get(),
|
||||
apiFetch.api.pengaduan.list.get({
|
||||
query: {
|
||||
status,
|
||||
search: value,
|
||||
take: "",
|
||||
page: "",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
const unsubscribe = subscribe(state, () => mutate());
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
async function handleRemove(id: string) {
|
||||
try {
|
||||
await apiFetch.api.credential.rm.delete({ id });
|
||||
showNotification({
|
||||
color: "teal",
|
||||
title: "Credential Deleted",
|
||||
message: "The credential was successfully removed.",
|
||||
});
|
||||
reloadState();
|
||||
} catch {
|
||||
showNotification({
|
||||
color: "red",
|
||||
title: "Error",
|
||||
message: "Failed to delete credential. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
@@ -100,87 +145,127 @@ function ListPengaduan() {
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c="dimmed">
|
||||
Loading credentials...
|
||||
Loading pengaduan...
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const list = data?.data?.list || [];
|
||||
const list = data?.data || [];
|
||||
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="xl"
|
||||
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={3} c="gray.2">
|
||||
Dompet Hilang
|
||||
</Title>
|
||||
<Group>
|
||||
<Title order={6} c="gray.5">
|
||||
#PGD-061125-001
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
updated 2 minutes ago
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Badge
|
||||
size="xl"
|
||||
variant="light"
|
||||
radius="sm"
|
||||
color="gray"
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
Antrian
|
||||
</Badge>
|
||||
<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 ? (
|
||||
<Flex justify="center" align="center" py={"xl"}>
|
||||
<Stack gap={4} align="center">
|
||||
<IconFileSad size={32} color="gray" />
|
||||
<Text c="dimmed" size="sm">
|
||||
No pengaduan have been added yet.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap="sm">
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconClockHour3 size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Tanggal Aduan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">
|
||||
05 November 2025
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Lokasi
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">
|
||||
Jalan Darmasaba Raya no 77
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconAlignJustified size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Detail
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis, obcaecati. Sint natus culpa temporibus neque quasi expedita ratione, facere optio incidunt quibusdam suscipit nam nemo delectus beatae similique velit obcaecati?
|
||||
</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
) : (
|
||||
list.map((v: any) => (
|
||||
<Card
|
||||
key={v.id}
|
||||
radius="lg"
|
||||
p="xl"
|
||||
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={3} c="gray.2">
|
||||
{v.title}
|
||||
</Title>
|
||||
<Group>
|
||||
<Title order={6} c="gray.5">
|
||||
#{v.noPengaduan}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{v.updatedAt}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Badge
|
||||
size="xl"
|
||||
variant="light"
|
||||
radius="sm"
|
||||
color={
|
||||
v.status === "diterima"
|
||||
? "green"
|
||||
: v.status === "ditolak"
|
||||
? "red"
|
||||
: v.status === "selesai"
|
||||
? "blue"
|
||||
: v.status === "dikerjakan"
|
||||
? "purple"
|
||||
: "yellow"
|
||||
}
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
{v.status}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap="sm">
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconClockHour3 size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Tanggal Aduan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.createdAt}</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Lokasi
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.location}</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconAlignJustified size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Detail
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.detail}</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
21
src/server/lib/get-last-updated.ts
Normal file
21
src/server/lib/get-last-updated.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function getLastUpdated(date: string | Date): string {
|
||||
const now = new Date();
|
||||
const updated = new Date(date);
|
||||
const diffMs = now.getTime() - updated.getTime();
|
||||
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffMinutes < 1) return "baru saja";
|
||||
if (diffMinutes < 60) return `${diffMinutes} menit lalu`;
|
||||
if (diffHours < 24) return `${diffHours} jam lalu`;
|
||||
if (diffDays < 7) return `${diffDays} hari lalu`;
|
||||
|
||||
// kalau sudah lebih dari seminggu, tampilkan tanggal
|
||||
return updated.toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
@@ -136,32 +136,65 @@ export async function catFile(config: Config, fileName: string): Promise<string>
|
||||
}
|
||||
|
||||
export async function uploadFile(config: Config, file: File): Promise<string> {
|
||||
const remoteName = path.basename(file.name);
|
||||
const remoteName = path.basename(file.name);
|
||||
|
||||
// 1. Dapatkan upload link (pakai Authorization)
|
||||
const uploadUrlResponse = await fetchWithAuth(
|
||||
config,
|
||||
`${config.URL}/${config.REPO}/upload-link/`
|
||||
);
|
||||
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
|
||||
// 1. Dapatkan upload link (pakai Authorization)
|
||||
const uploadUrlResponse = await fetchWithAuth(
|
||||
config,
|
||||
`${config.URL}/${config.REPO}/upload-link/`
|
||||
);
|
||||
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
|
||||
|
||||
// 2. Siapkan form-data
|
||||
const formData = new FormData();
|
||||
formData.append("parent_dir", "/");
|
||||
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
|
||||
formData.append("file", file, remoteName); // file langsung, jangan pakai Blob
|
||||
// 2. Siapkan form-data
|
||||
const formData = new FormData();
|
||||
formData.append("parent_dir", "/");
|
||||
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
|
||||
formData.append("file", file, remoteName); // file langsung, jangan pakai Blob
|
||||
|
||||
// 3. Upload file TANPA Authorization header, token di query param
|
||||
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
// 3. Upload file TANPA Authorization header, token di query param
|
||||
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
const text = await res.text();
|
||||
|
||||
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||
return `✅ Uploaded ${file.name} successfully`;
|
||||
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||
return `✅ Uploaded ${file.name} successfully`;
|
||||
}
|
||||
export async function uploadFileBase64(config: Config, base64File: { name: string; data: string }): Promise<string> {
|
||||
const remoteName = path.basename(base64File.name);
|
||||
|
||||
// 1. Dapatkan upload link (pakai Authorization)
|
||||
const uploadUrlResponse = await fetchWithAuth(
|
||||
config,
|
||||
`${config.URL}/${config.REPO}/upload-link/`
|
||||
);
|
||||
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
|
||||
|
||||
// 2. Konversi base64 ke Blob
|
||||
const binary = Buffer.from(base64File.data, "base64");
|
||||
const blob = new Blob([binary]);
|
||||
|
||||
// 3. Siapkan form-data
|
||||
const formData = new FormData();
|
||||
formData.append("parent_dir", "/");
|
||||
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
|
||||
formData.append("file", blob, remoteName);
|
||||
|
||||
// 4. Upload file TANPA Authorization header, token di query param
|
||||
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
|
||||
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||
return `✅ Uploaded ${base64File.name} successfully`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export async function removeFile(config: Config, fileName: string): Promise<string> {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Elysia, { t } from "elysia"
|
||||
import type { StatusPengaduan } from "generated/prisma"
|
||||
import { getLastUpdated } from "../lib/get-last-updated"
|
||||
import { generateNoPengaduan } from "../lib/no-pengaduan"
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { prisma } from "../lib/prisma"
|
||||
import { defaultConfigSF, uploadFile } from "../lib/seafile"
|
||||
import { defaultConfigSF, uploadFile, uploadFileBase64 } from "../lib/seafile"
|
||||
|
||||
const PengaduanRoute = new Elysia({
|
||||
prefix: "pengaduan",
|
||||
@@ -431,38 +432,201 @@ const PengaduanRoute = new Elysia({
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.post("/upload",
|
||||
async ({ body }) => {
|
||||
const { file } = body;
|
||||
.post("/upload", async ({ body }) => {
|
||||
const { file } = body;
|
||||
|
||||
// Validasi file
|
||||
if (!file) {
|
||||
return { success: false, message: "File tidak ditemukan" };
|
||||
}
|
||||
|
||||
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
|
||||
// const buffer = await file.arrayBuffer();
|
||||
const result = await uploadFile(defaultConfigSF, file);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Upload berhasil",
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
seafileResult: result
|
||||
};
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
file: t.File({ format: "binary" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File",
|
||||
description: "Tool untuk upload file ke Seafile",
|
||||
tags: ["mcp"],
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
// Validasi file
|
||||
if (!file) {
|
||||
return { success: false, message: "File tidak ditemukan" };
|
||||
}
|
||||
);
|
||||
|
||||
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
|
||||
// const buffer = await file.arrayBuffer();
|
||||
const result = await uploadFile(defaultConfigSF, file);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Upload berhasil",
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
seafileResult: result
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.File({ format: "binary" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File",
|
||||
description: "Tool untuk upload file ke Seafile",
|
||||
tags: ["mcp"],
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
})
|
||||
.post("/upload-base64", async ({ body }) => {
|
||||
const { file } = body;
|
||||
|
||||
// Validasi file
|
||||
if (!file) {
|
||||
return { success: false, message: "File tidak ditemukan" };
|
||||
}
|
||||
|
||||
// Konversi file ke base64
|
||||
const buffer = await file.arrayBuffer();
|
||||
const base64String = Buffer.from(buffer).toString("base64");
|
||||
|
||||
// (Opsional) jika perlu dikirim ke Seafile sebagai base64
|
||||
const result = await uploadFileBase64(defaultConfigSF, { name: file.name, data: base64String });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Upload berhasil",
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
base64Preview: base64String.slice(0, 100) + "...", // hanya preview
|
||||
seafileResult: result
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.File({ format: "binary" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File (Base64)",
|
||||
description: "Tool untuk upload file ke Seafile dalam format Base64",
|
||||
tags: ["mcp"],
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
})
|
||||
|
||||
.get("/list", async ({ query }) => {
|
||||
const { take, page, search, status } = query
|
||||
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
|
||||
let where: any = {
|
||||
isActive: true,
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
noPengaduan: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
Warga: {
|
||||
phone: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (status && status !== "semua") {
|
||||
where = {
|
||||
...where,
|
||||
status: status
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.pengaduan.findMany({
|
||||
skip,
|
||||
take: !take ? 10 : Number(take),
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
},
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
noPengaduan: true,
|
||||
title: true,
|
||||
detail: true,
|
||||
location: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
CategoryPengaduan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataFix = data.map((item) => {
|
||||
return {
|
||||
noPengaduan: item.noPengaduan,
|
||||
title: item.title,
|
||||
detail: item.detail,
|
||||
status: item.status,
|
||||
location: item.location,
|
||||
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||
}
|
||||
})
|
||||
|
||||
return dataFix
|
||||
}, {
|
||||
query: t.Object({
|
||||
take: t.String({ optional: true }),
|
||||
page: t.String({ optional: true }),
|
||||
search: t.String({ optional: true }),
|
||||
status: t.String({ optional: true }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "List Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan list pengaduan warga`,
|
||||
}
|
||||
})
|
||||
.get("/count", async ({ query }) => {
|
||||
const counts = await prisma.pengaduan.groupBy({
|
||||
by: ['status'],
|
||||
where: {
|
||||
isActive: true,
|
||||
},
|
||||
_count: {
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
const grouped = Object.fromEntries(
|
||||
counts.map(c => [c.status, c._count.status])
|
||||
);
|
||||
|
||||
const total = await prisma.pengaduan.count({
|
||||
where: { isActive: true },
|
||||
});
|
||||
|
||||
return {
|
||||
antrian: grouped?.antrian || 0,
|
||||
diterima: grouped?.diterima || 0,
|
||||
dikerjakan: grouped?.dikerjakan || 0,
|
||||
ditolak: grouped?.ditolak || 0,
|
||||
selesai: grouped?.selesai || 0,
|
||||
semua: total,
|
||||
};
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Jumlah Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan jumlah pengaduan warga`,
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default PengaduanRoute
|
||||
|
||||
2
upload_base64.sh
Normal file
2
upload_base64.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
curl -X POST http://localhost:3000/api/pengaduan/upload-base64 \
|
||||
-F file=@package.json
|
||||
Reference in New Issue
Block a user