diff --git a/src/api/division.ts b/src/api/division.ts index aa74b9c..dee655e 100644 --- a/src/api/division.ts +++ b/src/api/division.ts @@ -64,15 +64,36 @@ export const division = new Elysia({ }, ) .get( - "/metrics", + "/activities/stats", async ({ set }) => { try { - const metrics = await prisma.divisionMetric.findMany({ - include: { division: true }, - }); - return { data: metrics }; + // Get activity count by status + const [selesai, berjalan, tertunda, dibatalkan] = await Promise.all([ + prisma.activity.count({ where: { status: "SELESAI" } }), + prisma.activity.count({ where: { status: "BERJALAN" } }), + prisma.activity.count({ where: { status: "TERTUNDA" } }), + prisma.activity.count({ where: { status: "DIBATALKAN" } }), + ]); + + const total = selesai + berjalan + tertunda + dibatalkan; + + // Calculate percentages + const percentages = { + selesai: total > 0 ? (selesai / total) * 100 : 0, + berjalan: total > 0 ? (berjalan / total) * 100 : 0, + tertunda: total > 0 ? (tertunda / total) * 100 : 0, + dibatalkan: total > 0 ? (dibatalkan / total) * 100 : 0, + }; + + return { + data: { + total, + counts: { selesai, berjalan, tertunda, dibatalkan }, + percentages, + }, + }; } catch (error) { - logger.error({ error }, "Failed to fetch division metrics"); + logger.error({ error }, "Failed to fetch activity stats"); set.status = 500; return { error: "Internal Server Error" }; } @@ -80,10 +101,117 @@ export const division = new Elysia({ { response: { 200: t.Object({ - data: t.Array(t.Any()), + data: t.Object({ + total: t.Number(), + counts: t.Object({ + selesai: t.Number(), + berjalan: t.Number(), + tertunda: t.Number(), + dibatalkan: t.Number(), + }), + percentages: t.Object({ + selesai: t.Number(), + berjalan: t.Number(), + tertunda: t.Number(), + dibatalkan: t.Number(), + }), + }), }), 500: t.Object({ error: t.String() }), }, - detail: { summary: "Get division performance metrics" }, + detail: { summary: "Get activity statistics by status" }, + }, + ) + .get( + "/documents/stats", + async ({ set }) => { + try { + // Group documents by type + const [gambarCount, dokumenCount] = await Promise.all([ + prisma.document.count({ where: { type: "Gambar" } }), + prisma.document.count({ where: { type: "Dokumen" } }), + ]); + + return { + data: [ + { name: "Gambar", jumlah: gambarCount, color: "#FACC15" }, + { name: "Dokumen", jumlah: dokumenCount, color: "#22C55E" }, + ], + }; + } catch (error) { + logger.error({ error }, "Failed to fetch document stats"); + set.status = 500; + return { error: "Internal Server Error" }; + } + }, + { + response: { + 200: t.Object({ + data: t.Array( + t.Object({ + name: t.String(), + jumlah: t.Number(), + color: t.String(), + }), + ), + }), + 500: t.Object({ error: t.String() }), + }, + detail: { summary: "Get document statistics by type" }, + }, + ) + .get( + "/discussions", + async ({ set }) => { + try { + // Get recent discussions with sender info + const discussions = await prisma.discussion.findMany({ + where: { parentId: null }, // Only top-level discussions + include: { + sender: { + select: { name: true, email: true }, + }, + division: { + select: { name: true }, + }, + }, + orderBy: { createdAt: "desc" }, + take: 10, + }); + + // Format for frontend + const formattedDiscussions = discussions.map((d) => ({ + id: d.id, + message: d.message, + sender: d.sender.name || d.sender.email, + date: d.createdAt.toISOString(), + division: d.division?.name || null, + isResolved: d.isResolved, + })); + + return { data: formattedDiscussions }; + } catch (error) { + logger.error({ error }, "Failed to fetch discussions"); + set.status = 500; + return { error: "Internal Server Error" }; + } + }, + { + response: { + 200: t.Object({ + data: t.Array( + t.Object({ + id: t.String(), + message: t.String(), + sender: t.String(), + date: t.String(), + division: t.Nullable(t.String()), + isResolved: t.Boolean(), + }), + ), + }), + 500: t.Object({ error: t.String() }), + }, + detail: { summary: "Get recent discussions" }, }, ); diff --git a/src/components/kinerja-divisi/discussion-panel.tsx b/src/components/kinerja-divisi/discussion-panel.tsx index 2876c82..894634f 100644 --- a/src/components/kinerja-divisi/discussion-panel.tsx +++ b/src/components/kinerja-divisi/discussion-panel.tsx @@ -1,34 +1,51 @@ -import { Card, Group, Stack, Text, useMantineColorScheme } from "@mantine/core"; +import { Card, Group, Loader, Stack, Text, useMantineColorScheme } from "@mantine/core"; import { MessageCircle } from "lucide-react"; +import { useEffect, useState } from "react"; +import { apiClient } from "@/utils/api-client"; +import { format } from "date-fns"; +import { id } from "date-fns/locale"; interface DiscussionItem { + id: string; message: string; sender: string; date: string; + division: string | null; + isResolved: boolean; } -const discussions: DiscussionItem[] = [ - { - message: "Kepada Pelayanan, mohon di cek...", - sender: "I.B Surya Prabhawa Manu", - date: "12 Apr 2025", - }, - { - message: "Kepada staf perencanaan @suar...", - sender: "Ni Nyoman Yuliani", - date: "14 Jun 2025", - }, - { - message: "ijin atau mohon kepada KBD sar...", - sender: "Ni Wayan Martini", - date: "12 Apr 2025", - }, -]; - export function DiscussionPanel() { const { colorScheme } = useMantineColorScheme(); const dark = colorScheme === "dark"; + const [discussions, setDiscussions] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchDiscussions() { + try { + const res = await apiClient.GET("/api/division/discussions"); + if (res.data?.data) { + setDiscussions(res.data.data); + } + } catch (error) { + console.error("Failed to fetch discussions", error); + } finally { + setLoading(false); + } + } + + fetchDiscussions(); + }, []); + + const formatDate = (dateString: string) => { + try { + return format(new Date(dateString), "dd MMM yyyy", { locale: id }); + } catch { + return dateString; + } + }; + return ( - {discussions.map((discussion) => ( - - + + + ) : discussions.length > 0 ? ( + discussions.map((discussion) => ( + - {discussion.message} - - - - {discussion.sender} + + {discussion.message} - - {discussion.date} - - - - ))} + + + {discussion.sender} + {discussion.division && ( + + • {discussion.division} + + )} + + + {formatDate(discussion.date)} + + + + )) + ) : ( + + Tidak ada diskusi + + )} ); diff --git a/src/components/kinerja-divisi/document-chart.tsx b/src/components/kinerja-divisi/document-chart.tsx index 3f5c5bd..c281509 100644 --- a/src/components/kinerja-divisi/document-chart.tsx +++ b/src/components/kinerja-divisi/document-chart.tsx @@ -1,4 +1,5 @@ -import { Card, Text, useMantineColorScheme } from "@mantine/core"; +import { Card, Group, Loader, Text, useMantineColorScheme } from "@mantine/core"; +import { useEffect, useState } from "react"; import { Bar, BarChart, @@ -9,16 +10,38 @@ import { XAxis, YAxis, } from "recharts"; +import { apiClient } from "@/utils/api-client"; -const documentData = [ - { name: "Gambar", jumlah: 300, color: "#FACC15" }, - { name: "Dokumen", jumlah: 310, color: "#22C55E" }, -]; +interface DocumentData { + name: string; + jumlah: number; + color: string; +} export function DocumentChart() { const { colorScheme } = useMantineColorScheme(); const dark = colorScheme === "dark"; + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchDocumentStats() { + try { + const res = await apiClient.GET("/api/division/documents/stats"); + if (res.data?.data) { + setData(res.data.data); + } + } catch (error) { + console.error("Failed to fetch document stats", error); + } finally { + setLoading(false); + } + } + + fetchDocumentStats(); + }, []); + return ( Jumlah Dokumen - - - - - - - - {documentData.map((entry) => ( - - ))} - - - + {loading ? ( + + + + ) : data.length > 0 ? ( + + + + + + + + {data.map((entry) => ( + + ))} + + + + ) : ( + + + Tidak ada dokumen + + + )} ); } diff --git a/src/components/kinerja-divisi/progress-chart.tsx b/src/components/kinerja-divisi/progress-chart.tsx index 572fffb..afa113a 100644 --- a/src/components/kinerja-divisi/progress-chart.tsx +++ b/src/components/kinerja-divisi/progress-chart.tsx @@ -2,23 +2,68 @@ import { Box, Card, Group, + Loader, Stack, Text, useMantineColorScheme, } from "@mantine/core"; +import { useEffect, useState } from "react"; import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; +import { apiClient } from "@/utils/api-client"; -const progressData = [ - { name: "Selesai", value: 83.33, color: "#22C55E" }, - { name: "Dikerjakan", value: 16.67, color: "#F59E0B" }, - { name: "Segera Dikerjakan", value: 0, color: "#3B82F6" }, - { name: "Dibatalkan", value: 0, color: "#EF4444" }, -]; +interface ProgressData { + name: string; + value: number; + color: string; +} + +interface ActivityStats { + total: number; + counts: { + selesai: number; + berjalan: number; + tertunda: number; + dibatalkan: number; + }; + percentages: { + selesai: number; + berjalan: number; + tertunda: number; + dibatalkan: number; + }; +} export function ProgressChart() { const { colorScheme } = useMantineColorScheme(); const dark = colorScheme === "dark"; + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchActivityStats() { + try { + const res = await apiClient.GET("/api/division/activities/stats"); + if (res.data?.data) { + const stats = res.data.data as ActivityStats; + const chartData: ProgressData[] = [ + { name: "Selesai", value: stats.percentages.selesai, color: "#22C55E" }, + { name: "Dikerjakan", value: stats.percentages.berjalan, color: "#F59E0B" }, + { name: "Segera Dikerjakan", value: stats.percentages.tertunda, color: "#3B82F6" }, + { name: "Dibatalkan", value: stats.percentages.dibatalkan, color: "#EF4444" }, + ]; + setData(chartData); + } + } catch (error) { + console.error("Failed to fetch activity stats", error); + } finally { + setLoading(false); + } + } + + fetchActivityStats(); + }, []); + return ( Progres Kegiatan - - - - {progressData.map((entry) => ( - - ))} - - - - - - {progressData.map((item) => ( - - - + + + ) : ( + <> + + + + {data.map((entry) => ( + + ))} + + - - {item.name} - - - - {item.value.toFixed(2)}% - - - ))} - + + + + {data.map((item) => ( + + + + + {item.name} + + + + {item.value.toFixed(2)}% + + + ))} + + + )} ); }