diff --git a/bun.lock b/bun.lock
index 64afd89..2640de7 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "jenna-mcp",
@@ -21,6 +22,8 @@
"@types/lodash": "^4.17.20",
"@types/uuid": "^11.0.0",
"add": "^2.0.6",
+ "echarts": "^6.0.0",
+ "echarts-for-react": "^3.0.5",
"elysia": "^1.4.15",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.3",
@@ -289,6 +292,10 @@
"ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="],
+ "echarts": ["echarts@6.0.0", "", { "dependencies": { "tslib": "2.3.0", "zrender": "6.0.0" } }, "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ=="],
+
+ "echarts-for-react": ["echarts-for-react@3.0.5", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "size-sensor": "^1.0.1" }, "peerDependencies": { "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "react": "^15.0.0 || >=16.0.0" } }, "sha512-YpEI5Ty7O/2nvCfQ7ybNa+S90DwE8KYZWacGvJW4luUqywP7qStQ+pxDlYOmr4jGDu10mhEkiAuMKcUlT4W5vg=="],
+
"editor": ["editor@1.0.0", "", {}, "sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
@@ -645,6 +652,8 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+ "size-sensor": ["size-sensor@1.0.2", "", {}, "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw=="],
+
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
@@ -687,7 +696,7 @@
"tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="],
- "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ "tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
@@ -743,6 +752,8 @@
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
+ "zrender": ["zrender@6.0.0", "", { "dependencies": { "tslib": "2.3.0" } }, "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg=="],
+
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
"body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@@ -769,12 +780,22 @@
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+ "react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"request/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"request/qs": ["qs@6.5.3", "", {}, "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="],
"request/uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
+ "use-callback-ref/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "use-sidecar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
diff --git a/package.json b/package.json
index d283974..1cb8768 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,8 @@
"@types/lodash": "^4.17.20",
"@types/uuid": "^11.0.0",
"add": "^2.0.6",
+ "echarts": "^6.0.0",
+ "echarts-for-react": "^3.0.5",
"elysia": "^1.4.15",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.3",
diff --git a/src/components/DashboardCountData.tsx b/src/components/DashboardCountData.tsx
new file mode 100644
index 0000000..8da6172
--- /dev/null
+++ b/src/components/DashboardCountData.tsx
@@ -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 (
+
+
+ }
+ 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"}
+ />
+
+
+ }
+ 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"}
+ />
+
+
+ }
+ label="Warga"
+ value={String(data?.data?.warga)}
+ color="blue"
+ />
+
+
+ );
+}
+
+
+function MetricCard({
+ icon,
+ label,
+ value,
+ change,
+ color,
+}: {
+ icon: React.ReactNode;
+ label: string;
+ value: string;
+ change?: string;
+ color: string;
+}) {
+ return (
+
+ (e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
+ }
+ onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
+ >
+
+
+ {icon}
+
+ {label}
+
+
+
+
+ {value}
+
+ {change && (
+
+ {change}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/DashboardGrafik.tsx b/src/components/DashboardGrafik.tsx
new file mode 100644
index 0000000..ef31883
--- /dev/null
+++ b/src/components/DashboardGrafik.tsx
@@ -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({});
+ 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 (
+
+
+
+
+ System Performance
+
+
+
+
+
+
+ {/*
+
+
+ */}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/DashboardLastData.tsx b/src/components/DashboardLastData.tsx
new file mode 100644
index 0000000..c881b77
--- /dev/null
+++ b/src/components/DashboardLastData.tsx
@@ -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 (
+
+
+
+
+
+ Last update pengaduan
+
+
+
+
+ {
+ data && Array.isArray(data.pengaduan) && data.pengaduan.length > 0 ? data.pengaduan.map((item: any, index: number) => (
+
+ )) : Tidak ada data
+ }
+
+
+
+
+
+
+
+
+ Last update pelayanan surat
+
+
+
+
+ {
+ data && Array.isArray(data.pelayanan) && data.pelayanan.length > 0 ? data.pelayanan.map((item: any, index: number) => (
+
+ )) : Tidak ada data
+ }
+
+
+
+
+ );
+}
+
+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 (
+ navigate(kategori == "pelayanan" ? `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${id}` : `/scr/dashboard/pengaduan/detail?id=${id}`)}
+ >
+
+
+
+ {judul}
+
+
+
+ #{nomer} ∙ {updated}
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index 211dc38..1622581 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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)
diff --git a/src/pages/scr/dashboard/dashboard_home.tsx b/src/pages/scr/dashboard/dashboard_home.tsx
index 5c3f085..554cd4e 100644
--- a/src/pages/scr/dashboard/dashboard_home.tsx
+++ b/src/pages/scr/dashboard/dashboard_home.tsx
@@ -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
- }
- style={{
- boxShadow: "0 0 12px rgba(0,255,200,0.3)",
- }}
- >
- View Details
-
-
-
-
- }
- label="Active Users"
- value="1,248"
- change="+12%"
- color="teal"
- />
-
-
- }
- label="Server Uptime"
- value="99.98%"
- change="+0.02%"
- color="cyan"
- />
-
-
- }
- label="Database Ops"
- value="82.4K"
- change="+5.6%"
- color="blue"
- />
-
-
- }
- label="System Health"
- value="Stable"
- change=""
- color="green"
- />
-
-
-
-
-
-
-
- System Performance
-
-
-
-
-
- Resource usage and performance indicators.
-
-
-
-
-
-
-
-
-
+
+
+
);
}
-function MetricCard({
- icon,
- label,
- value,
- change,
- color,
-}: {
- icon: React.ReactNode;
- label: string;
- value: string;
- change?: string;
- color: string;
-}) {
- return (
-
- (e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
- }
- onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
- >
-
-
- {icon}
-
- {label}
-
-
-
-
- {value}
-
- {change && (
-
- {change}
-
- )}
-
-
-
- );
-}
-
function ProgressSection({
label,
value,
diff --git a/src/server/routes/dashboard_route.ts b/src/server/routes/dashboard_route.ts
new file mode 100644
index 0000000..d568b6f
--- /dev/null
+++ b/src/server/routes/dashboard_route.ts
@@ -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 = {};
+
+ 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