[darmasaba-dashboard][2026-03-27] feat: modular seeders and database-backed dashboard

- Split seeders into modular files per feature category
- Added seed:auth, seed:demographics, seed:divisions, seed:services, seed:dashboard commands
- Connected dashboard components to live database (Budget, SDGs, Satisfaction)
- Added API endpoints: /api/dashboard/budget, /api/dashboard/sdgs, /api/dashboard/satisfaction
- Updated prisma schema with dashboard metrics models
- Added loading states to dashboard components
- Fixed header navigation to /admin

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-27 12:14:19 +08:00
parent 34804127c5
commit 44b6b158ef
17 changed files with 1616 additions and 416 deletions

View File

@@ -1,11 +1,13 @@
import {
Card,
Group,
Loader,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { useEffect, useState } from "react";
import {
Bar,
BarChart,
@@ -15,18 +17,44 @@ import {
XAxis,
YAxis,
} from "recharts";
import { apiClient } from "@/utils/api-client";
const apbdesData = [
{ name: "Belanja", value: 70, color: "#3B82F6" },
{ name: "Pangan", value: 45, color: "#22C55E" },
{ name: "Pembiayaan", value: 55, color: "#FACC15" },
{ name: "Pendapatan", value: 90, color: "#3B82F6" },
];
interface ApbdesData {
name: string;
value: number;
color: string;
}
export function ChartAPBDes() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const [data, setData] = useState<ApbdesData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchApbdes() {
try {
const res = await apiClient.GET("/api/dashboard/budget");
if (res.data?.data) {
setData(
res.data.data.map((d) => ({
name: d.category,
value: d.percentage,
color: d.color,
})),
);
}
} catch (error) {
console.error("Failed to fetch APBDes data", error);
} finally {
setLoading(false);
}
}
fetchApbdes();
}, []);
return (
<Card
p="md"
@@ -45,33 +73,39 @@ export function ChartAPBDes() {
Grafik APBDes
</Title>
<Stack gap="xs">
{apbdesData.map((item) => (
<Group key={item.name} align="center" gap="md">
<Text size="sm" fw={500} w={100} c={dark ? "white" : "gray.7"}>
{item.name}
</Text>
<ResponsiveContainer width="100%" height={20}>
<BarChart layout="vertical" data={[item]}>
<XAxis type="number" hide domain={[0, 100]} />
<YAxis type="category" hide dataKey="name" />
<Tooltip
formatter={(value: number | string | undefined) => [
`${value}%`,
"",
]}
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
/>
<Bar dataKey="value" radius={[4, 4, 4, 4]}>
<Cell fill={item.color} />
</Bar>
</BarChart>
</ResponsiveContainer>
{loading ? (
<Group justify="center" py="xl">
<Loader />
</Group>
))}
) : data.length > 0 ? (
data.map((item) => (
<Group key={item.name} align="center" gap="md">
<Text size="sm" fw={500} w={100} c={dark ? "white" : "gray.7"}>
{item.name}
</Text>
<ResponsiveContainer width="100%" height={12} style={{ flex: 1 }}>
<BarChart
layout="vertical"
data={[item]}
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
>
<XAxis type="number" hide domain={[0, 100]} />
<YAxis type="category" hide dataKey="name" />
<Bar dataKey="value" radius={[10, 10, 10, 10]} barSize={12}>
<Cell fill={item.color} />
</Bar>
</BarChart>
</ResponsiveContainer>
<Text size="sm" fw={600} w={40} ta="right" c={dark ? "white" : "gray.9"}>
{item.value}%
</Text>
</Group>
))
) : (
<Text size="sm" c="dimmed" ta="center">
Tidak ada data APBDes
</Text>
)}
</Stack>
</Card>
);

View File

@@ -2,23 +2,51 @@ import {
Box,
Card,
Group,
Loader,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { useEffect, useState } from "react";
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
import { apiClient } from "@/utils/api-client";
const satisfactionData = [
{ name: "Sangat Puas", value: 25, color: "#4E5BA6" },
{ name: "Puas", value: 25, color: "#F4C542" },
{ name: "Cukup", value: 25, color: "#8CC63F" },
{ name: "Kurang", value: 25, color: "#E57373" },
];
interface SatisfactionData {
name: string;
value: number;
color: string;
}
export function SatisfactionChart() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const [data, setData] = useState<SatisfactionData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchSatisfaction() {
try {
const res = await apiClient.GET("/api/dashboard/satisfaction");
if (res.data?.data) {
setData(
res.data.data.map((d) => ({
name: d.category,
value: d.value,
color: d.color,
})),
);
}
} catch (error) {
console.error("Failed to fetch satisfaction data", error);
} finally {
setLoading(false);
}
}
fetchSatisfaction();
}, []);
return (
<Card
p="md"
@@ -40,31 +68,37 @@ export function SatisfactionChart() {
Tingkat kepuasan layanan
</Text>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={satisfactionData}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={120}
paddingAngle={2}
dataKey="value"
>
{satisfactionData.map((entry) => (
<Cell key={`cell-${entry.name}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
/>
</PieChart>
{loading ? (
<Group justify="center" align="center" h="100%">
<Loader />
</Group>
) : (
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={120}
paddingAngle={2}
dataKey="value"
>
{data.map((entry) => (
<Cell key={`cell-${entry.name}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
/>
</PieChart>
)}
</ResponsiveContainer>
<Group justify="center" gap="md" mt="md">
{satisfactionData.map((item) => (
{data.map((item) => (
<Group key={item.name} gap="xs">
<Box
w={12}