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 - - - - - } - 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