refactor: modularize dashboard components per PromptDashboard.md
- Create reusable StatCard component for header metrics
- Create ChartSurat component for bar chart (surat statistics)
- Create DivisionProgress component for divisi teraktif
- Create ChartAPBDes component for APBDes horizontal bar chart
- Create ActivityList component for calendar events
- Create SatisfactionChart component for donut chart
- Create SDGSCard component for SDGs metrics
- Refactor DashboardContent to use new modular components
- Add proper dark mode support with specified colors
- Implement responsive grid layout (12/6/1 columns)
- Add custom SDGs icons (Energy, Peace, Health, Poverty, Ocean)
New components structure:
src/components/dashboard/
- stat-card.tsx
- chart-surat.tsx
- chart-apbdes.tsx
- division-progress.tsx
- activity-list.tsx
- satisfaction-chart.tsx
- sdgs-card.tsx
- index.ts (exports)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -1,523 +1,242 @@
|
||||
import {
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
FileText,
|
||||
MessageCircle,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip, // Added Tooltip import
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { Grid, Stack, useMantineColorScheme } from "@mantine/core";
|
||||
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
|
||||
import { ActivityList } from "./dashboard/activity-list";
|
||||
import { ChartAPBDes } from "./dashboard/chart-apbdes";
|
||||
import { ChartSurat } from "./dashboard/chart-surat";
|
||||
import { DivisionProgress } from "./dashboard/division-progress";
|
||||
import { SatisfactionChart } from "./dashboard/satisfaction-chart";
|
||||
import { SDGSCard } from "./dashboard/sdgs-card";
|
||||
import { StatCard } from "./dashboard/stat-card";
|
||||
|
||||
// Import Mantine components
|
||||
// SDGs Icons
|
||||
function EnergyIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24 4L14 24H22L20 44L34 20H26L24 4Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Card, // Added for icon containers
|
||||
Grid,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineColorScheme, // Add this import
|
||||
} from "@mantine/core";
|
||||
function PeaceIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="24" cy="24" r="20" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M24 4V44M24 24L10 38M24 24L38 38"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const barChartData = [
|
||||
{ month: "Jan", value: 145 },
|
||||
{ month: "Feb", value: 165 },
|
||||
{ month: "Mar", value: 195 },
|
||||
{ month: "Apr", value: 155 },
|
||||
{ month: "Mei", value: 205 },
|
||||
{ month: "Jun", value: 185 },
|
||||
];
|
||||
function HealthIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24 44C24 44 6 28 6 18C6 11.373 11.373 6 18 6C21.5 6 24.5 7.5 24 12C23.5 7.5 26.5 6 30 6C36.627 6 42 11.373 42 18C42 28 24 44 24 44Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const pieChartData = [
|
||||
{ name: "Puas", value: 25 },
|
||||
{ name: "Cukup", value: 25 },
|
||||
{ name: "Kurang", value: 25 },
|
||||
{ name: "Sangat puas", value: 25 },
|
||||
];
|
||||
function PovertyIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="6" y="18" width="36" height="26" rx="2" fill="currentColor" />
|
||||
<path
|
||||
d="M14 18V12C14 8.686 16.686 6 20 6H28C31.314 6 34 8.686 34 12V18"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const COLORS = ["#4E5BA6", "#F4C542", "#8CC63F", "#E57373"];
|
||||
function OceanIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 30C6 30 10 26 14 30C18 34 22 30 26 30C30 30 34 34 38 30C42 26 46 30 46 30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M6 38C6 38 10 34 14 38C18 42 22 38 26 38C30 38 34 42 38 38C42 34 46 38 46 38"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="24" cy="16" r="6" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const divisiData = [
|
||||
{ name: "Kesejahteraan", value: 37 },
|
||||
{ name: "Pemerintahan", value: 26 },
|
||||
{ name: "Keuangan", value: 17 },
|
||||
{ name: "Sekretaris Desa", value: 15 },
|
||||
];
|
||||
|
||||
const eventData = [
|
||||
{ date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" },
|
||||
{ date: "15 Oktober 2025", title: "Davest" },
|
||||
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
|
||||
const sdgsData = [
|
||||
{
|
||||
title: "Desa Berenergi Bersih dan Terbarukan",
|
||||
score: 99.64,
|
||||
icon: <EnergyIcon />,
|
||||
color: "#FACC15",
|
||||
bgColor: "#FEF9C3",
|
||||
},
|
||||
{
|
||||
title: "Desa Damai Berkeadilan",
|
||||
score: 78.65,
|
||||
icon: <PeaceIcon />,
|
||||
color: "#3B82F6",
|
||||
bgColor: "#DBEAFE",
|
||||
},
|
||||
{
|
||||
title: "Desa Sehat dan Sejahtera",
|
||||
score: 77.37,
|
||||
icon: <HealthIcon />,
|
||||
color: "#22C55E",
|
||||
bgColor: "#DCFCE7",
|
||||
},
|
||||
{
|
||||
title: "Desa Tanpa Kemiskinan",
|
||||
score: 52.62,
|
||||
icon: <PovertyIcon />,
|
||||
color: "#EF4444",
|
||||
bgColor: "#FEE2E2",
|
||||
},
|
||||
{
|
||||
title: "Desa Peduli Lingkungan Laut",
|
||||
score: 50.0,
|
||||
icon: <OceanIcon />,
|
||||
color: "#06B6D4",
|
||||
bgColor: "#CFFAFE",
|
||||
},
|
||||
];
|
||||
|
||||
export function DashboardContent() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Stats Cards */}
|
||||
{/* Header Metrics - 4 Stat Cards */}
|
||||
<Grid gutter="md">
|
||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
h="100%"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Surat Minggu Ini
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700}>
|
||||
99
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
14 baru, 14 diproses
|
||||
</Text>
|
||||
<Text size="sm" c="red" mt="xs">
|
||||
12% dari minggu lalu ↗ +12%
|
||||
</Text>
|
||||
</Box>
|
||||
<ThemeIcon
|
||||
variant="filled"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
color={dark ? "gray" : "darmasaba-blue"}
|
||||
>
|
||||
<FileText style={{ width: "70%", height: "70%" }} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<StatCard
|
||||
title="Surat Minggu Ini"
|
||||
value={99}
|
||||
detail="14 baru, 14 diproses"
|
||||
trend="12% dari minggu lalu ↗ +12%"
|
||||
trendValue={12}
|
||||
icon={<FileText style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
h="100%"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Pengaduan Aktif
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700}>
|
||||
28
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
14 baru, 14 diproses
|
||||
</Text>
|
||||
</Box>
|
||||
<ThemeIcon
|
||||
variant="filled"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
color={dark ? "gray" : "darmasaba-blue"}
|
||||
>
|
||||
<MessageCircle style={{ width: "70%", height: "70%" }} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<StatCard
|
||||
title="Pengaduan Aktif"
|
||||
value={28}
|
||||
detail="14 baru, 14 diproses"
|
||||
icon={<MessageCircle style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
h="100%"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Layanan Selesai
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700}>
|
||||
156
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
bulan ini
|
||||
</Text>
|
||||
<Text size="sm" c="red" mt="xs">
|
||||
+8%
|
||||
</Text>
|
||||
</Box>
|
||||
<ThemeIcon
|
||||
variant="filled"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
color={dark ? "gray" : "darmasaba-blue"}
|
||||
>
|
||||
<CheckCircle style={{ width: "70%", height: "70%" }} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<StatCard
|
||||
title="Layanan Selesai"
|
||||
value={156}
|
||||
detail="bulan ini"
|
||||
trend="+8%"
|
||||
trendValue={8}
|
||||
icon={<CheckCircle style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
h="100%"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Kepuasan Warga
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700}>
|
||||
87.2%
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
dari 482 responden
|
||||
</Text>
|
||||
</Box>
|
||||
<ThemeIcon
|
||||
variant="filled"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
color={dark ? "gray" : "darmasaba-blue"}
|
||||
>
|
||||
<Users style={{ width: "70%", height: "70%" }} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<StatCard
|
||||
title="Kepuasan Warga"
|
||||
value="87.2%"
|
||||
detail="dari 482 responden"
|
||||
icon={<Users style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Section 2: Chart & Division Progress */}
|
||||
<Grid gutter="lg">
|
||||
{/* Bar Chart */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Box>
|
||||
<Title order={4} mb={5}>
|
||||
Statistik Pengajuan Surat
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
Trend pengajuan surat 6 bulan terakhir
|
||||
</Text>
|
||||
</Box>
|
||||
<ActionIcon variant="subtle" size="lg" radius="md">
|
||||
{/* Original SVG converted to a generic Icon placeholder */}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 5L13 10L8 15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={barChartData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="var(--mantine-color-gray-3)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "var(--mantine-color-text)" }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
ticks={[0, 55, 110, 165, 220]}
|
||||
tick={{ fill: "var(--mantine-color-text)" }}
|
||||
/>
|
||||
<Tooltip />
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--mantine-color-blue-filled)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<ChartSurat />
|
||||
</Grid.Col>
|
||||
|
||||
{/* Pie Chart */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Title order={4} mb={5}>
|
||||
Tingkat Kepuasan
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Tingkat kepuasan layanan
|
||||
</Text>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieChartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={80}
|
||||
outerRadius={120}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{pieChartData.map((_entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<Group justify="center" gap="md" mt="md">
|
||||
<Group gap="xs">
|
||||
<Box
|
||||
w={12}
|
||||
h={12}
|
||||
style={{ backgroundColor: COLORS[0], borderRadius: "50%" }}
|
||||
/>
|
||||
<Text size="sm">Sangat puas (0%)</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Box
|
||||
w={12}
|
||||
h={12}
|
||||
style={{ backgroundColor: COLORS[1], borderRadius: "50%" }}
|
||||
/>
|
||||
<Text size="sm">Puas (0%)</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Box
|
||||
w={12}
|
||||
h={12}
|
||||
style={{ backgroundColor: COLORS[2], borderRadius: "50%" }}
|
||||
/>
|
||||
<Text size="sm">Cukup (0%)</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Box
|
||||
w={12}
|
||||
h={12}
|
||||
style={{ backgroundColor: COLORS[3], borderRadius: "50%" }}
|
||||
/>
|
||||
<Text size="sm">Kurang (0%)</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<DivisionProgress />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Bottom Section */}
|
||||
{/* Section 3: APBDes Chart */}
|
||||
<ChartAPBDes />
|
||||
|
||||
{/* Section 4 & 5: Activity List & Satisfaction Chart */}
|
||||
<Grid gutter="lg">
|
||||
{/* Divisi Teraktif */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Group gap="xs" mb="lg">
|
||||
<Box>
|
||||
{/* Original SVG icon */}
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="7"
|
||||
height="7"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x="3"
|
||||
y="14"
|
||||
width="7"
|
||||
height="7"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x="14"
|
||||
y="3"
|
||||
width="7"
|
||||
height="7"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x="14"
|
||||
y="14"
|
||||
width="7"
|
||||
height="7"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Title order={4}>Divisi Teraktif</Title>
|
||||
</Group>
|
||||
<Stack gap="sm">
|
||||
{divisiData.map((divisi, index) => (
|
||||
<Box key={index}>
|
||||
<Group justify="space-between" mb={5}>
|
||||
<Text size="sm" fw={500}>
|
||||
{divisi.name}
|
||||
</Text>
|
||||
<Text size="sm" fw={600}>
|
||||
{divisi.value} Kegiatan
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={(divisi.value / 37) * 100}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
color="blue"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
<ActivityList />
|
||||
</Grid.Col>
|
||||
|
||||
{/* Kalender */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Group gap="xs" mb="lg">
|
||||
<Calendar style={{ width: 20, height: 20 }} />
|
||||
<Title order={4}>Kalender & Kegiatan Mendatang</Title>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
{eventData.map((event, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
style={{
|
||||
borderLeft: "4px solid var(--mantine-color-blue-filled)",
|
||||
paddingLeft: 12,
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c="dimmed">
|
||||
{event.date}
|
||||
</Text>
|
||||
<Text fw={500}>{event.title}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
<SatisfactionChart />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* APBDes Chart */}
|
||||
<Card
|
||||
p="md"
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
>
|
||||
<Title order={4} mb="lg">
|
||||
Grafik APBDes
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
<Group align="center" gap="md">
|
||||
<Text size="sm" fw={500} w={60}>
|
||||
Belanja
|
||||
</Text>
|
||||
<Progress
|
||||
value={70}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
color="blue"
|
||||
style={{ flex: 1 }}
|
||||
{/* Section 6: SDGs Desa Cards */}
|
||||
<Grid gutter="md">
|
||||
{sdgsData.map((sdg, index) => (
|
||||
<Grid.Col key={index} span={{ base: 12, sm: 6, md: 4, lg: 2.4 }}>
|
||||
<SDGSCard
|
||||
title={sdg.title}
|
||||
score={sdg.score}
|
||||
icon={sdg.icon}
|
||||
color={sdg.color}
|
||||
bgColor={sdg.bgColor}
|
||||
/>
|
||||
</Group>
|
||||
<Group align="center" gap="md">
|
||||
<Text size="sm" fw={500} w={60}>
|
||||
Pendapatan
|
||||
</Text>
|
||||
<Progress
|
||||
value={90}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
color="green"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</Group>
|
||||
<Group align="center" gap="md">
|
||||
<Text size="sm" fw={500} w={60}>
|
||||
Pembangunan
|
||||
</Text>
|
||||
<Progress
|
||||
value={50}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
color="orange"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
69
src/components/dashboard/activity-list.tsx
Normal file
69
src/components/dashboard/activity-list.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { Calendar } from "lucide-react";
|
||||
|
||||
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";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
>
|
||||
<Group gap="xs" mb="lg">
|
||||
<Calendar
|
||||
style={{ width: 20, height: 20 }}
|
||||
color={dark ? "#E2E8F0" : "#1E3A5F"}
|
||||
/>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||
Kalender & Kegiatan Mendatang
|
||||
</Title>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
{events.map((event, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
style={{
|
||||
borderLeft: "4px solid var(--mantine-color-blue-filled)",
|
||||
paddingLeft: 12,
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c="dimmed">
|
||||
{event.date}
|
||||
</Text>
|
||||
<Text fw={500} c={dark ? "white" : "gray.9"}>
|
||||
{event.title}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
74
src/components/dashboard/chart-apbdes.tsx
Normal file
74
src/components/dashboard/chart-apbdes.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
Cell,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
export function ChartAPBDes() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||
Grafik APBDes
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
{apbdesData.map((item, index) => (
|
||||
<Group key={index} 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) => [`${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>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
110
src/components/dashboard/chart-surat.tsx
Normal file
110
src/components/dashboard/chart-surat.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
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 },
|
||||
];
|
||||
|
||||
export function ChartSurat() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Box>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
||||
Statistik Pengajuan Surat
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
Trend pengajuan surat 6 bulan terakhir
|
||||
</Text>
|
||||
</Box>
|
||||
<ActionIcon variant="subtle" size="lg" radius="md">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 5L13 10L8 15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
ticks={[0, 55, 110, 165, 220]}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--mantine-color-blue-filled)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
69
src/components/dashboard/division-progress.tsx
Normal file
69
src/components/dashboard/division-progress.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
|
||||
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";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||
Divisi Teraktif
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{divisionData.map((divisi, index) => (
|
||||
<Box key={index}>
|
||||
<Group justify="space-between" mb={5}>
|
||||
<Text size="sm" fw={500} c={dark ? "white" : "gray.7"}>
|
||||
{divisi.name}
|
||||
</Text>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||
{divisi.value} Kegiatan
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={(divisi.value / max_value) * 100}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
color="blue"
|
||||
animated
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
7
src/components/dashboard/index.ts
Normal file
7
src/components/dashboard/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { ActivityList } from "./activity-list";
|
||||
export { ChartAPBDes } from "./chart-apbdes";
|
||||
export { ChartSurat } from "./chart-surat";
|
||||
export { DivisionProgress } from "./division-progress";
|
||||
export { SatisfactionChart } from "./satisfaction-chart";
|
||||
export { SDGSCard } from "./sdgs-card";
|
||||
export { StatCard } from "./stat-card";
|
||||
82
src/components/dashboard/satisfaction-chart.tsx
Normal file
82
src/components/dashboard/satisfaction-chart.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
export function SatisfactionChart() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
||||
Tingkat Kepuasan
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
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, index) => (
|
||||
<Cell key={`cell-${index}`} 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, index) => (
|
||||
<Group key={index} gap="xs">
|
||||
<Box
|
||||
w={12}
|
||||
h={12}
|
||||
style={{ backgroundColor: item.color, borderRadius: "50%" }}
|
||||
/>
|
||||
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
53
src/components/dashboard/sdgs-card.tsx
Normal file
53
src/components/dashboard/sdgs-card.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Box, Card, Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface SDGSCardProps {
|
||||
title: string;
|
||||
score: number;
|
||||
icon: ReactNode;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
}
|
||||
|
||||
export function SDGSCard({
|
||||
title,
|
||||
score,
|
||||
icon,
|
||||
color,
|
||||
bgColor,
|
||||
}: SDGSCardProps) {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={bgColor}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : bgColor,
|
||||
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c={dark ? "white" : "gray.8"} fw={500} mb="xs">
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={color}>
|
||||
{score.toFixed(2)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
style={{
|
||||
color,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
86
src/components/dashboard/stat-card.tsx
Normal file
86
src/components/dashboard/stat-card.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
detail?: string;
|
||||
trend?: string;
|
||||
trendValue?: number;
|
||||
icon: ReactNode;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
export function StatCard({
|
||||
title,
|
||||
value,
|
||||
detail,
|
||||
trend,
|
||||
trendValue,
|
||||
icon,
|
||||
iconColor = "darmasaba-blue",
|
||||
}: StatCardProps) {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
const isPositiveTrend = trendValue ? trendValue >= 0 : true;
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
{title}
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||
{value}
|
||||
</Text>
|
||||
</Group>
|
||||
{detail && (
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
{detail}
|
||||
</Text>
|
||||
)}
|
||||
{trend && (
|
||||
<Text
|
||||
size="sm"
|
||||
c={isPositiveTrend ? "green" : "red"}
|
||||
mt="xs"
|
||||
fw={500}
|
||||
>
|
||||
{trend}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<ThemeIcon
|
||||
variant="filled"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
color={dark ? "gray" : iconColor}
|
||||
>
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user