update: dashboard admin

Deskripsi:
- menu dashboard
- api dashboard

No Issues
This commit is contained in:
2025-11-27 17:55:01 +08:00
parent c622565bb7
commit cca1840922
8 changed files with 626 additions and 149 deletions

View File

@@ -0,0 +1,98 @@
import apiFetch from "@/lib/apiFetch";
import { Card, Flex, Grid, Group, Stack, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconFileCertificate, IconMessageReport, IconUsers } from "@tabler/icons-react";
import useSWR from "swr";
export default function DashboardCountData() {
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api.dashboard.count.get()
);
useShallowEffect(() => {
mutate();
}, []);
return (
<Grid>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconMessageReport size={28} />}
label="Pengaduan Hari Ini"
value={String(data?.data?.pengaduan?.today)}
change={String(data?.data?.pengaduan?.kenaikan) + "%"}
color={(data?.data?.pengaduan?.kenaikan || 0) > 0 ? "teal" : "gray"}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconFileCertificate size={28} />}
label="Pengajuan Surat Hari Ini"
value={String(data?.data?.pelayanan?.today)}
change={String(data?.data?.pelayanan?.kenaikan) + "%"}
color={(data?.data?.pelayanan?.kenaikan || 0) > 0 ? "teal" : "gray"}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconUsers size={28} />}
label="Warga"
value={String(data?.data?.warga)}
color="blue"
/>
</Grid.Col>
</Grid>
);
}
function MetricCard({
icon,
label,
value,
change,
color,
}: {
icon: React.ReactNode;
label: string;
value: string;
change?: string;
color: string;
}) {
return (
<Card
radius="lg"
p="md"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))",
borderColor: "rgba(100,100,100,0.2)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
>
<Stack gap={6}>
<Group gap={6}>
{icon}
<Text size="sm" c="dimmed">
{label}
</Text>
</Group>
<Flex align="center" justify="space-between">
<Text fw={600} size="xl" c="gray.0">
{value}
</Text>
{change && (
<Text size="sm" c={color}>
{change}
</Text>
)}
</Flex>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,83 @@
import apiFetch from "@/lib/apiFetch";
import { Card, Divider, Flex, Stack, Title } from "@mantine/core";
import { IconSettings } from "@tabler/icons-react";
import type { EChartsOption } from "echarts";
import EChartsReact from "echarts-for-react";
import { useEffect, useState } from "react";
import useSWR from "swr";
export default function DashboardGrafik() {
const [options, setOptions] = useState<EChartsOption>({});
const { data, mutate, isLoading } = useSWR(
"grafik-dashboard",
async () => {
return apiFetch.api.dashboard.grafik.get().then(res => res.data);
}
);
const loadData = () => {
if (!data) return;
const option: EChartsOption = {
darkMode: true,
animation: true,
legend: {
textStyle: { color: "#fff" } // warna legend putih
},
tooltip: {},
dataset: {
dimensions: data.dimensions,
source: data.source
},
xAxis: {
type: "category",
axisLabel: { color: "#fff" }
},
yAxis: {
type: "value",
minInterval: 1
},
color: ["#1abc9c", "#10816aff"],
series: [
{ type: "bar" },
{ type: "bar" }
]
};
setOptions(option);
};
useEffect(() => {
if (data) loadData();
}, [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 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.0">
System Performance
</Title>
<IconSettings size={20} color="gray" />
</Flex>
<Divider my="xs" />
<Stack gap="sm">
<EChartsReact style={{ height: 400, width: "100%" }} option={options} />
{/* <ProgressSection label="CPU Usage" value={68} color="teal" />
<ProgressSection label="Memory Usage" value={75} color="cyan" />
<ProgressSection label="Network Load" value={42} color="blue" />
<ProgressSection label="Disk Space" value={88} color="red" /> */}
</Stack>
</Stack>
</Card>
)
}

View File

@@ -0,0 +1,133 @@
import apiFetch from "@/lib/apiFetch";
import { Badge, Button, Card, Flex, Group, Stack, Text, Title, Tooltip } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { useNavigate } from "react-router-dom";
import useSWR from "swr";
export default function DashboardLastData() {
const navigate = useNavigate();
const { data, mutate, isLoading } = useSWR("last-update", async () => {
const res = await apiFetch.api.dashboard["last-update"].get();
return res.data
});
useShallowEffect(() => {
mutate();
}, []);
return (
<Flex justify="flex-start" gap="md">
<Card
radius="lg"
p="xl"
withBorder
w={"50%"}
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 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex align="center" pb={"sm"} justify="space-between" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
<Title order={4} c="gray.0">
Last update pengaduan
</Title>
<Button variant="subtle" size="xs" radius="md" onClick={() => navigate(`/scr/dashboard/pengaduan/list`)}>View All</Button>
</Flex>
<Stack gap="sm" mt="md" align="stretch" justify="center">
{
data && Array.isArray(data.pengaduan) && data.pengaduan.length > 0 ? data.pengaduan.map((item: any, index: number) => (
<PengaduanSection
key={index}
id={item.id}
nomer={item.noPengaduan}
judul={item.title}
status={item.status}
updated={item.updatedAt}
kategori="pengaduan"
/>
)) : <Text c="dimmed" ta={"center"} >Tidak ada data</Text>
}
</Stack>
</Stack>
</Card>
<Card
radius="lg"
p="xl"
withBorder
w={"50%"}
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 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex align="center" pb={"sm"} justify="space-between" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
<Title order={4} c="gray.0">
Last update pelayanan surat
</Title>
<Button variant="subtle" size="xs" radius="md" onClick={() => navigate(`/scr/dashboard/pelayanan-surat/list-pelayanan`)}>View All</Button>
</Flex>
<Stack gap="sm" mt="md" align="stretch" justify="center">
{
data && Array.isArray(data.pelayanan) && data.pelayanan.length > 0 ? data.pelayanan.map((item: any, index: number) => (
<PengaduanSection
key={index}
id={item.id}
nomer={item.noPengaduan}
judul={item.title}
status={item.status}
updated={item.updatedAt}
kategori="pelayanan"
/>
)) : <Text c="dimmed" ta={"center"} >Tidak ada data</Text>
}
</Stack>
</Stack>
</Card>
</Flex>
);
}
function PengaduanSection({ id, nomer, judul, status, updated, kategori }: { id: string, nomer: string, judul: string, status: string, updated: string, kategori: 'pengaduan' | 'pelayanan' }) {
const navigate = useNavigate();
return (
<Stack
gap="xs"
onClick={() => navigate(kategori == "pelayanan" ? `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${id}` : `/scr/dashboard/pengaduan/detail?id=${id}`)}
>
<Flex align="center" pb={"sm"} justify="space-between" gap="md" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
<Flex direction={"column"}>
<Text size="md" c="gray.2" lineClamp={1}>
{judul}
</Text>
<Group>
<Text size="sm" c="dimmed">
#{nomer} {updated}
</Text>
</Group>
</Flex>
<Tooltip label={status}>
<Badge size="xs" circle color={
status === "diterima"
? "green"
: status === "ditolak"
? "red"
: status === "selesai"
? "blue"
: status === "dikerjakan"
? "gray"
: "yellow"
} />
</Tooltip>
</Flex>
</Stack>
)
}