update: dashboard admin
Deskripsi: - menu dashboard - api dashboard No Issues
This commit is contained in:
98
src/components/DashboardCountData.tsx
Normal file
98
src/components/DashboardCountData.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
83
src/components/DashboardGrafik.tsx
Normal file
83
src/components/DashboardGrafik.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
133
src/components/DashboardLastData.tsx
Normal file
133
src/components/DashboardLastData.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -10,14 +10,15 @@ import Auth from "./server/routes/auth_route";
|
||||
import ConfigurationDesaRoute from "./server/routes/configuration_desa_route";
|
||||
import CredentialRoute from "./server/routes/credential_route";
|
||||
import DarmasabaRoute from "./server/routes/darmasaba_route";
|
||||
import DashboardRoute from "./server/routes/dashboard_route";
|
||||
import LayananRoute from "./server/routes/layanan_route";
|
||||
import { MCPRoute } from "./server/routes/mcp_route";
|
||||
import PelayananRoute from "./server/routes/pelayanan_surat_route";
|
||||
import PengaduanRoute from "./server/routes/pengaduan_route";
|
||||
import SuratRoute from "./server/routes/surat_route";
|
||||
import TestPengaduanRoute from "./server/routes/test_pengaduan";
|
||||
import UserRoute from "./server/routes/user_route";
|
||||
import WargaRoute from "./server/routes/warga_route";
|
||||
import SuratRoute from "./server/routes/surat_route";
|
||||
|
||||
const Docs = new Elysia({
|
||||
tags: ["docs"],
|
||||
@@ -31,6 +32,7 @@ const Api = new Elysia({
|
||||
prefix: "/api",
|
||||
tags: ["api"],
|
||||
})
|
||||
.use(DashboardRoute)
|
||||
.use(PengaduanRoute)
|
||||
.use(PelayananRoute)
|
||||
.use(ConfigurationDesaRoute)
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import DashboardCountData from "@/components/DashboardCountData";
|
||||
import DashboardGrafik from "@/components/DashboardGrafik";
|
||||
import DashboardLastData from "@/components/DashboardLastData";
|
||||
import {
|
||||
Card,
|
||||
Badge,
|
||||
Container,
|
||||
Flex,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Progress,
|
||||
Badge,
|
||||
Button,
|
||||
Grid,
|
||||
Divider,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconActivity,
|
||||
IconUsers,
|
||||
IconServer,
|
||||
IconDatabase,
|
||||
IconSettings,
|
||||
IconArrowRight,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
@@ -43,144 +34,15 @@ export default function Dashboard() {
|
||||
Live
|
||||
</Badge>
|
||||
</Group>
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: "teal", to: "cyan", deg: 45 }}
|
||||
radius="md"
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
style={{
|
||||
boxShadow: "0 0 12px rgba(0,255,200,0.3)",
|
||||
}}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
icon={<IconUsers size={28} />}
|
||||
label="Active Users"
|
||||
value="1,248"
|
||||
change="+12%"
|
||||
color="teal"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
icon={<IconServer size={28} />}
|
||||
label="Server Uptime"
|
||||
value="99.98%"
|
||||
change="+0.02%"
|
||||
color="cyan"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
icon={<IconDatabase size={28} />}
|
||||
label="Database Ops"
|
||||
value="82.4K"
|
||||
change="+5.6%"
|
||||
color="blue"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
icon={<IconActivity size={28} />}
|
||||
label="System Health"
|
||||
value="Stable"
|
||||
change=""
|
||||
color="green"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<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" />
|
||||
<Text size="sm" c="dimmed">
|
||||
Resource usage and performance indicators.
|
||||
</Text>
|
||||
<Stack gap="sm" mt="md">
|
||||
<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>
|
||||
<DashboardCountData />
|
||||
<DashboardGrafik />
|
||||
<DashboardLastData />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressSection({
|
||||
label,
|
||||
value,
|
||||
|
||||
276
src/server/routes/dashboard_route.ts
Normal file
276
src/server/routes/dashboard_route.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import Elysia from "elysia";
|
||||
import { getLastUpdated } from "../lib/get-last-updated";
|
||||
import { prisma } from "../lib/prisma";
|
||||
|
||||
const DashboardRoute = new Elysia({
|
||||
prefix: "dashboard",
|
||||
tags: ["dashboard"],
|
||||
})
|
||||
|
||||
.get("/count", async () => {
|
||||
// ---- RANGE HARI INI ----
|
||||
const now = new Date();
|
||||
|
||||
const startOfToday = new Date(now);
|
||||
startOfToday.setHours(0, 0, 0, 0);
|
||||
|
||||
const endOfToday = new Date(now);
|
||||
endOfToday.setHours(23, 59, 59, 999);
|
||||
|
||||
// ---- RANGE KEMARIN ----
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const startOfYesterday = new Date(yesterday);
|
||||
startOfYesterday.setHours(0, 0, 0, 0);
|
||||
|
||||
const endOfYesterday = new Date(yesterday);
|
||||
endOfYesterday.setHours(23, 59, 59, 999);
|
||||
|
||||
// ---- QUERY ----
|
||||
|
||||
const dataWarga = await prisma.warga.count();
|
||||
|
||||
// Pengaduan
|
||||
const dataPengaduanToday = await prisma.pengaduan.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: "antrian",
|
||||
createdAt: {
|
||||
gte: startOfToday,
|
||||
lte: endOfToday,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dataPengaduanYesterday = await prisma.pengaduan.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: "antrian",
|
||||
createdAt: {
|
||||
gte: startOfYesterday,
|
||||
lte: endOfYesterday,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const kenaikanPengaduan =
|
||||
dataPengaduanYesterday === 0
|
||||
? dataPengaduanToday > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((dataPengaduanToday - dataPengaduanYesterday) / dataPengaduanYesterday) * 100;
|
||||
|
||||
// Pelayanan
|
||||
const dataPelayananToday = await prisma.pelayananAjuan.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: "antrian",
|
||||
createdAt: {
|
||||
gte: startOfToday,
|
||||
lte: endOfToday,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dataPelayananYesterday = await prisma.pelayananAjuan.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: "antrian",
|
||||
createdAt: {
|
||||
gte: startOfYesterday,
|
||||
lte: endOfYesterday,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const kenaikanPelayanan =
|
||||
dataPelayananYesterday === 0
|
||||
? dataPelayananToday > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((dataPelayananToday - dataPelayananYesterday) / dataPelayananYesterday) * 100;
|
||||
|
||||
// ---- FINAL OUTPUT ----
|
||||
|
||||
const dataFix = {
|
||||
warga: dataWarga,
|
||||
pengaduan: {
|
||||
today: dataPengaduanToday,
|
||||
yesterday: dataPengaduanYesterday,
|
||||
kenaikan: Number(kenaikanPengaduan.toFixed(2)), // dalam persen
|
||||
},
|
||||
pelayanan: {
|
||||
today: dataPelayananToday,
|
||||
yesterday: dataPelayananYesterday,
|
||||
kenaikan: Number(kenaikanPelayanan.toFixed(2)), // dalam persen
|
||||
},
|
||||
};
|
||||
|
||||
return dataFix;
|
||||
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Dashboard - Menghitung Data",
|
||||
description: `tool untuk menghitung data pengaduan dan pelayanan yg masuk hari ini dan data warga`,
|
||||
}
|
||||
})
|
||||
.get("/last-update", async () => {
|
||||
const dataPengaduan = await prisma.pengaduan.findMany({
|
||||
skip: 0,
|
||||
take: 5,
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
})
|
||||
|
||||
const dataPengaduanFix = dataPengaduan.map((item) => {
|
||||
return {
|
||||
noPengaduan: item.noPengaduan,
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
status: item.status,
|
||||
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||
}
|
||||
})
|
||||
|
||||
const dataPelayanan = await prisma.pelayananAjuan.findMany({
|
||||
skip: 0,
|
||||
take: 5,
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
updatedAt: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataPelayananFix = dataPelayanan.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.CategoryPelayanan.name,
|
||||
status: item.status,
|
||||
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const dataFix = {
|
||||
pengaduan: dataPengaduanFix,
|
||||
pelayanan: dataPelayananFix,
|
||||
}
|
||||
|
||||
return dataFix;
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Dashboard - List data pengaduan dan pelayanan terupdate",
|
||||
description: `tool untuk mendapatkan list data pengaduan dan pelayanan yg terupdate`,
|
||||
}
|
||||
})
|
||||
.get("/grafik", async () => {
|
||||
const now = new Date();
|
||||
|
||||
const start7Days = new Date(now);
|
||||
start7Days.setDate(start7Days.getDate() - 7);
|
||||
start7Days.setHours(0, 0, 0, 0);
|
||||
|
||||
const endToday = new Date(now);
|
||||
endToday.setHours(23, 59, 59, 999);
|
||||
|
||||
// Ambil semua data pengaduan & pelayanan dalam 7 hari
|
||||
const pengaduan = await prisma.pengaduan.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: start7Days,
|
||||
lte: endToday,
|
||||
},
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const pelayanan = await prisma.pelayananAjuan.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: start7Days,
|
||||
lte: endToday,
|
||||
},
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
|
||||
// --- BUAT RANGE TANGGAL 7 HARI ---
|
||||
const resultMap: Record<string, { pengaduan: number; pelayanan: number }> = {};
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const d = new Date(start7Days);
|
||||
d.setDate(d.getDate() + i);
|
||||
|
||||
const formatted = d.toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long"
|
||||
});
|
||||
|
||||
resultMap[formatted] = { pengaduan: 0, pelayanan: 0 };
|
||||
}
|
||||
|
||||
// --- HITUNG PENGADUAN PER HARI ---
|
||||
pengaduan.forEach((item) => {
|
||||
const t = item.createdAt.toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long"
|
||||
});
|
||||
|
||||
if (resultMap[t]) {
|
||||
resultMap[t].pengaduan += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// --- HITUNG PELAYANAN PER HARI ---
|
||||
pelayanan.forEach((item) => {
|
||||
const t = item.createdAt.toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long"
|
||||
});
|
||||
|
||||
if (resultMap[t]) {
|
||||
resultMap[t].pelayanan += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// --- KONVERSI KE FORMAT FINAL ---
|
||||
const source = Object.keys(resultMap).map((tanggal) => ({
|
||||
tanggal,
|
||||
pengaduan: resultMap[tanggal]?.pengaduan,
|
||||
pelayanan: resultMap[tanggal]?.pelayanan,
|
||||
}));
|
||||
|
||||
return {
|
||||
dimensions: ["tanggal", "pengaduan", "pelayanan"],
|
||||
source,
|
||||
};
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Dashboard - Grafik data pengaduan dan pelayanan",
|
||||
description: `tool untuk mendapatkan grafik data pengaduan dan pelayanan`,
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
;
|
||||
|
||||
export default DashboardRoute
|
||||
Reference in New Issue
Block a user