diff --git a/generated/api.ts b/generated/api.ts index 043b3d5..aaa60da 100644 --- a/generated/api.ts +++ b/generated/api.ts @@ -239,6 +239,40 @@ export interface paths { patch?: never; trace?: never; }; + "/api/complaint/service-trends": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get service letter trends for last 6 months */ + get: operations["getApiComplaintService-trends"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/complaint/service-weekly": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get service letter count for current week */ + get: operations["getApiComplaintService-weekly"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/resident/stats": { parameters: { query?: never; @@ -966,6 +1000,40 @@ export interface operations { }; }; }; + "getApiComplaintService-trends": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + "getApiComplaintService-weekly": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; getApiResidentStats: { parameters: { query?: never; diff --git a/generated/schema.json b/generated/schema.json index 5f74e5d..e001a13 100644 --- a/generated/schema.json +++ b/generated/schema.json @@ -1477,6 +1477,24 @@ } } }, + "/api/complaint/service-trends": { + "get": { + "operationId": "getApiComplaintService-trends", + "summary": "Get service letter trends for last 6 months", + "responses": { + "200": {} + } + } + }, + "/api/complaint/service-weekly": { + "get": { + "operationId": "getApiComplaintService-weekly", + "summary": "Get service letter count for current week", + "responses": { + "200": {} + } + } + }, "/api/resident/stats": { "get": { "operationId": "getApiResidentStats", diff --git a/src/api/complaint.ts b/src/api/complaint.ts index 6247e4c..c272982 100644 --- a/src/api/complaint.ts +++ b/src/api/complaint.ts @@ -82,4 +82,56 @@ export const complaint = new Elysia({ { detail: { summary: "Get recent innovation ideas" }, }, + ) + .get( + "/service-trends", + async ({ set }) => { + try { + // Get last 6 months trends for service letters + const trends = await prisma.$queryRaw` + SELECT + TO_CHAR("createdAt", 'Mon') as month, + EXTRACT(MONTH FROM "createdAt") as month_num, + COUNT(*) as count + FROM service_letter + WHERE "createdAt" > NOW() - INTERVAL '6 months' + GROUP BY month, month_num + ORDER BY month_num ASC + `; + return { data: trends }; + } catch (error) { + logger.error({ error }, "Failed to fetch service trends"); + set.status = 500; + return { error: "Internal Server Error" }; + } + }, + { + detail: { summary: "Get service letter trends for last 6 months" }, + }, + ) + .get( + "/service-weekly", + async ({ set }) => { + try { + const startOfWeek = new Date(); + startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay()); + startOfWeek.setHours(0, 0, 0, 0); + + const count = await prisma.serviceLetter.count({ + where: { + createdAt: { + gte: startOfWeek, + }, + }, + }); + return { data: { count } }; + } catch (error) { + logger.error({ error }, "Failed to fetch weekly service stats"); + set.status = 500; + return { error: "Internal Server Error" }; + } + }, + { + detail: { summary: "Get service letter count for current week" }, + }, ); diff --git a/src/components/dashboard-content.tsx b/src/components/dashboard-content.tsx index a8c2c9b..5299138 100644 --- a/src/components/dashboard-content.tsx +++ b/src/components/dashboard-content.tsx @@ -37,16 +37,20 @@ export function DashboardContent() { const [stats, setStats] = useState({ complaints: { total: 0, baru: 0, proses: 0, selesai: 0 }, residents: { total: 0, heads: 0, poor: 0 }, + weeklyService: 0, loading: true, }); useEffect(() => { async function fetchStats() { try { - const [complaintRes, residentRes] = await Promise.all([ - apiClient.GET("/api/complaint/stats"), - apiClient.GET("/api/resident/stats"), - ]); + const [complaintRes, residentRes, weeklyServiceRes] = await Promise.all( + [ + apiClient.GET("/api/complaint/stats"), + apiClient.GET("/api/resident/stats"), + apiClient.GET("/api/complaint/service-weekly"), + ], + ); setStats({ complaints: (complaintRes.data as any)?.data || { @@ -60,6 +64,7 @@ export function DashboardContent() { heads: 0, poor: 0, }, + weeklyService: (weeklyServiceRes.data as any)?.data?.count || 0, loading: false, }); } catch (error) { @@ -78,8 +83,8 @@ export function DashboardContent() { } diff --git a/src/components/dashboard/activity-list.tsx b/src/components/dashboard/activity-list.tsx index 9c1e95b..19ae81c 100644 --- a/src/components/dashboard/activity-list.tsx +++ b/src/components/dashboard/activity-list.tsx @@ -2,28 +2,51 @@ import { Box, Card, Group, + Loader, Stack, Text, Title, useMantineColorScheme, } from "@mantine/core"; +import dayjs from "dayjs"; import { Calendar } from "lucide-react"; +import { useEffect, useState } from "react"; +import { apiClient } from "@/utils/api-client"; interface EventData { date: string; title: string; } -const events: EventData[] = [ - { date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" }, - { date: "15 Oktober 2025", title: "Davest" }, - { date: "19 Oktober 2025", title: "Rapat Koordinasi" }, -]; - export function ActivityList() { const { colorScheme } = useMantineColorScheme(); const dark = colorScheme === "dark"; + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchEvents() { + try { + const res = await apiClient.GET("/api/event/"); + if (res.data?.data) { + setData( + (res.data.data as any[]).map((e) => ({ + date: dayjs(e.startDate).format("D MMMM YYYY"), + title: e.title, + })), + ); + } + } catch (error) { + console.error("Failed to fetch events", error); + } finally { + setLoading(false); + } + } + + fetchEvents(); + }, []); + return ( - {events.map((event) => ( - - - {event.date} - - - {event.title} - - - ))} + {loading ? ( + + + + ) : data.length > 0 ? ( + data.map((event) => ( + + + {event.date} + + + {event.title} + + + )) + ) : ( + + Tidak ada kegiatan mendatang + + )} ); diff --git a/src/components/dashboard/chart-surat.tsx b/src/components/dashboard/chart-surat.tsx index d61cefc..0d69950 100644 --- a/src/components/dashboard/chart-surat.tsx +++ b/src/components/dashboard/chart-surat.tsx @@ -3,10 +3,12 @@ import { Box, Card, Group, + Loader, Text, Title, useMantineColorScheme, } from "@mantine/core"; +import { useEffect, useState } from "react"; import { Bar, BarChart, @@ -16,20 +18,37 @@ import { XAxis, YAxis, } from "recharts"; - -const chartData = [ - { month: "Jan", value: 150 }, - { month: "Feb", value: 165 }, - { month: "Mar", value: 195 }, - { month: "Apr", value: 160 }, - { month: "Mei", value: 205 }, - { month: "Jun", value: 185 }, -]; +import { apiClient } from "@/utils/api-client"; export function ChartSurat() { const { colorScheme } = useMantineColorScheme(); const dark = colorScheme === "dark"; + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchTrends() { + try { + const res = await apiClient.GET("/api/complaint/service-trends"); + if (res.data?.data) { + setData( + (res.data.data as any[]).map((d) => ({ + month: d.month, + value: Number(d.count), + })), + ); + } + } catch (error) { + console.error("Failed to fetch service trends", error); + } finally { + setLoading(false); + } + } + + fetchTrends(); + }, []); + return ( - - - - - - - + {loading ? ( + + + + ) : ( + + + + + + + + )} ); diff --git a/src/components/dashboard/division-progress.tsx b/src/components/dashboard/division-progress.tsx index fbf90e5..10ee3aa 100644 --- a/src/components/dashboard/division-progress.tsx +++ b/src/components/dashboard/division-progress.tsx @@ -2,31 +2,52 @@ import { Box, Card, Group, + Loader, Progress, Stack, Text, Title, useMantineColorScheme, } from "@mantine/core"; +import { useEffect, useState } from "react"; +import { apiClient } from "@/utils/api-client"; interface DivisionData { name: string; value: number; } -const divisionData: DivisionData[] = [ - { name: "Kesejahteraan", value: 37 }, - { name: "Pemberdayaan", value: 26 }, - { name: "Keuangan", value: 17 }, - { name: "Sekretaris Desa", value: 15 }, -]; - -const max_value = 37; - export function DivisionProgress() { const { colorScheme } = useMantineColorScheme(); const dark = colorScheme === "dark"; + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchDivisions() { + try { + const res = await apiClient.GET("/api/division/"); + if (res.data?.data) { + setData( + (res.data.data as any[]).map((d) => ({ + name: d.name, + value: d._count?.activities || 0, + })), + ); + } + } catch (error) { + console.error("Failed to fetch division stats", error); + } finally { + setLoading(false); + } + } + + fetchDivisions(); + }, []); + + const max_value = Math.max(...data.map((d) => d.value), 1); + return ( - {divisionData.map((divisi) => ( - - - - {divisi.name} - - - {divisi.value} Kegiatan - - - - - ))} + {loading ? ( + + + + ) : data.length > 0 ? ( + data.map((divisi) => ( + + + + {divisi.name} + + + {divisi.value} Kegiatan + + + + + )) + ) : ( + + Tidak ada data divisi + + )} );