344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
import {
|
|
default as clientRoute,
|
|
default as clientRoutes,
|
|
} from "@/clientRoutes";
|
|
import apiFetch from "@/lib/apiFetch";
|
|
import {
|
|
ActionIcon,
|
|
AppShell,
|
|
Avatar,
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
Divider,
|
|
Flex,
|
|
Group,
|
|
NavLink,
|
|
Paper,
|
|
ScrollArea,
|
|
Stack,
|
|
Text,
|
|
Title,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import { useLocalStorage } from "@mantine/hooks";
|
|
import {
|
|
IconChevronLeft,
|
|
IconChevronRight,
|
|
IconDashboard,
|
|
IconFileCertificate,
|
|
IconKey,
|
|
IconLock,
|
|
IconMessageReport,
|
|
IconSettings,
|
|
IconUser,
|
|
IconUsersGroup,
|
|
} from "@tabler/icons-react";
|
|
import type { User } from "generated/prisma";
|
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
|
import { useEffect, useState } from "react";
|
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
|
|
|
function Logout() {
|
|
return (
|
|
<Group justify="center" mt="sm">
|
|
<Button
|
|
variant="gradient"
|
|
gradient={{ from: "red", to: "orange", deg: 60 }}
|
|
size="xs"
|
|
radius="md"
|
|
onClick={async () => {
|
|
await apiFetch.auth.logout.delete();
|
|
localStorage.removeItem("token");
|
|
window.location.href = "/login";
|
|
}}
|
|
style={{
|
|
boxShadow: "0 0 10px rgba(255,100,100,0.25)",
|
|
transition: "transform 0.2s ease",
|
|
}}
|
|
>
|
|
Log Out
|
|
</Button>
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
export default function DashboardLayout() {
|
|
const [opened, setOpened] = useLocalStorage({
|
|
key: "nav_open",
|
|
defaultValue: true,
|
|
});
|
|
|
|
return (
|
|
<AppShell
|
|
padding="lg"
|
|
navbar={{
|
|
width: 300,
|
|
breakpoint: "sm",
|
|
collapsed: { mobile: !opened, desktop: !opened },
|
|
}}
|
|
style={{
|
|
background: "radial-gradient(circle at top, #0a0a0a, #101010 70%)",
|
|
color: "#f8f9fa",
|
|
}}
|
|
>
|
|
<AppShell.Navbar
|
|
style={{
|
|
background: "linear-gradient(180deg, #141414, #1e1e1e)",
|
|
borderRight: "1px solid rgba(255,255,255,0.05)",
|
|
boxShadow: "2px 0 10px rgba(0,0,0,0.6)",
|
|
}}
|
|
>
|
|
<AppShell.Section>
|
|
<Group justify="flex-end" p="xs">
|
|
<Tooltip
|
|
label={opened ? "Collapse navigation" : "Expand navigation"}
|
|
withArrow
|
|
>
|
|
<ActionIcon
|
|
variant="light"
|
|
color="gray"
|
|
onClick={() => setOpened((v) => !v)}
|
|
radius="xl"
|
|
size="lg"
|
|
style={{
|
|
backgroundColor: "rgba(255,255,255,0.05)",
|
|
boxShadow: "0 0 6px hsla(167, 100%, 50%, 0.20), 0.20)",
|
|
}}
|
|
>
|
|
{opened ? <IconChevronLeft /> : <IconChevronRight />}
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
</AppShell.Section>
|
|
|
|
<AppShell.Section grow component={ScrollArea} flex={1}>
|
|
<NavigationDashboard />
|
|
</AppShell.Section>
|
|
|
|
<AppShell.Section>
|
|
<HostView />
|
|
</AppShell.Section>
|
|
</AppShell.Navbar>
|
|
|
|
<AppShell.Main>
|
|
<Stack gap="md">
|
|
<Paper
|
|
withBorder
|
|
radius="lg"
|
|
p="md"
|
|
style={{
|
|
background: "linear-gradient(145deg, #181818, #202020)",
|
|
border: "1px solid rgba(255,255,255,0.05)",
|
|
boxShadow: "0 0 15px rgba(0,255,200,0.08)",
|
|
}}
|
|
>
|
|
<Flex align="center" gap="md">
|
|
{!opened && (
|
|
<Tooltip label="Open navigation menu" withArrow>
|
|
<ActionIcon
|
|
variant="light"
|
|
color="gray"
|
|
onClick={() => setOpened(true)}
|
|
radius="xl"
|
|
>
|
|
<IconChevronRight />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
)}
|
|
<Title order={3} fw={600} c="gray.0">
|
|
Dashboard Control Panel
|
|
</Title>
|
|
</Flex>
|
|
</Paper>
|
|
<Outlet />
|
|
</Stack>
|
|
</AppShell.Main>
|
|
</AppShell>
|
|
);
|
|
}
|
|
|
|
function HostView() {
|
|
const [host, setHost] = useState<User | null>(null);
|
|
|
|
useEffect(() => {
|
|
async function fetchHost() {
|
|
const { data } = await apiFetch.api.user.find.get();
|
|
setHost(data?.user ?? null);
|
|
}
|
|
fetchHost();
|
|
}, []);
|
|
|
|
return (
|
|
<Card
|
|
radius="lg"
|
|
withBorder
|
|
shadow="sm"
|
|
p="md"
|
|
style={{
|
|
background: "linear-gradient(145deg, #181818, #212121)",
|
|
borderColor: "rgba(255,255,255,0.05)",
|
|
}}
|
|
>
|
|
{host ? (
|
|
<Stack gap="sm">
|
|
<Flex gap="md" align="center">
|
|
<Avatar size="md" radius="xl" color="teal" variant="filled">
|
|
{host.name?.[0]?.toUpperCase()}
|
|
</Avatar>
|
|
<Stack gap={2}>
|
|
<Text fw={600} c="gray.0">
|
|
{host.name}
|
|
</Text>
|
|
<Text size="sm" c="dimmed">
|
|
{host.roleId}
|
|
</Text>
|
|
</Stack>
|
|
</Flex>
|
|
<Divider my="xs" color="rgba(255,255,255,0.1)" />
|
|
<Logout />
|
|
</Stack>
|
|
) : (
|
|
<Flex align="center" justify="center" direction="column" p="md">
|
|
<IconUser size={28} color="gray" />
|
|
<Text size="sm" c="dimmed" mt={4}>
|
|
No user information available
|
|
</Text>
|
|
</Flex>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function NavigationDashboard() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
|
|
|
useEffect(() => {
|
|
async function fetchPermissions() {
|
|
const { data } = await apiFetch.api.user.find.get();
|
|
if (Array.isArray(data?.permissions)) {
|
|
setPermissions(data.permissions);
|
|
} else {
|
|
setPermissions([]);
|
|
}
|
|
}
|
|
fetchPermissions();
|
|
}, []);
|
|
|
|
const isActive = (path: keyof typeof clientRoute) =>
|
|
location.pathname.startsWith(clientRoute[path]);
|
|
|
|
const navItems = [
|
|
{
|
|
key: "dashboard",
|
|
path: "/scr/dashboard/dashboard-home",
|
|
icon: <IconDashboard size={20} />,
|
|
label: "Dashboard Overview",
|
|
description: "Quick summary and insights",
|
|
},
|
|
{
|
|
key: "pengaduan",
|
|
path: "/scr/dashboard/pengaduan/list",
|
|
icon: <IconMessageReport size={20} />,
|
|
label: "Pengaduan Warga",
|
|
description: "Manage pengaduan warga",
|
|
},
|
|
{
|
|
key: "pelayanan",
|
|
path: "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
|
icon: <IconFileCertificate size={20} />,
|
|
label: "Pelayanan Surat",
|
|
description: "Manage pelayanan surat",
|
|
},
|
|
{
|
|
key: "warga",
|
|
path: "/scr/dashboard/warga/list-warga",
|
|
icon: <IconUsersGroup size={20} />,
|
|
label: "Warga",
|
|
description: "Manage warga",
|
|
},
|
|
{
|
|
key: "setting",
|
|
path: "/scr/dashboard/setting/detail-setting",
|
|
icon: <IconSettings size={20} />,
|
|
label: "Setting",
|
|
description:
|
|
"Manage setting (category pengaduan dan pelayanan surat, desa, etc)",
|
|
},
|
|
{
|
|
key: "api_key",
|
|
path: "/scr/dashboard/apikey/apikey",
|
|
icon: <IconKey size={20} />,
|
|
label: "API Key Manager",
|
|
description: "Create and manage API keys",
|
|
},
|
|
{
|
|
key: "credential",
|
|
path: "/scr/dashboard/credential/credential",
|
|
icon: <IconLock size={20} />,
|
|
label: "Credentials",
|
|
description: "Manage service credentials",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Stack gap="xs" p="sm">
|
|
{navItems
|
|
.filter((item) => permissions.includes(item.key))
|
|
.map((item) => (
|
|
<NavLink
|
|
key={item.path}
|
|
active={isActive(item.path as keyof typeof clientRoute) ||
|
|
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
|
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
|
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")}
|
|
leftSection={item.icon}
|
|
label={
|
|
<Flex align="center" gap={6}>
|
|
<Text fw={500}>{item.label}</Text>
|
|
{(
|
|
isActive(item.path as keyof typeof clientRoute) ||
|
|
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
|
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
|
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
|
|
)
|
|
&& (
|
|
<Badge
|
|
variant="light"
|
|
color="teal"
|
|
radius="sm"
|
|
size="xs"
|
|
style={{ textTransform: "none" }}
|
|
>
|
|
Active
|
|
</Badge>
|
|
)}
|
|
</Flex>
|
|
}
|
|
description={item.description}
|
|
onClick={() =>
|
|
navigate(clientRoutes[item.path as keyof typeof clientRoute])
|
|
}
|
|
style={{
|
|
backgroundColor: isActive(item.path as keyof typeof clientRoute) ||
|
|
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
|
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
|
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
|
|
? "rgba(0,255,200,0.1)"
|
|
: "transparent",
|
|
borderRadius: "8px",
|
|
transition: "all 0.2s ease",
|
|
}}
|
|
styles={{
|
|
label: { color: "white" },
|
|
description: { color: "#aaa" },
|
|
section: { color: "teal" },
|
|
}}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
);
|
|
}
|