feat(noc): implement sync management UI and backend integration

This commit is contained in:
2026-03-30 14:48:47 +08:00
parent 3125bc1002
commit 65844bac7e
28 changed files with 2558 additions and 1339 deletions

View File

@@ -1,4 +1,4 @@
import { Grid, Image, Loader, Stack, Center } from "@mantine/core";
import { Center, Grid, Image, Loader, Stack } from "@mantine/core";
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
import { useEffect, useState } from "react";
import { apiClient } from "@/utils/api-client";
@@ -18,20 +18,21 @@ export function DashboardContent() {
loading: true,
});
const [sdgsData, setSdgsData] = useState<{ title: string; score: number; image: string | null }[]>([]);
const [sdgsData, setSdgsData] = useState<
{ title: string; score: number; image: string | null }[]
>([]);
const [sdgsLoading, setSdgsLoading] = useState(true);
useEffect(() => {
async function fetchStats() {
try {
const [complaintRes, residentRes, weeklyServiceRes, sdgsRes] = await Promise.all(
[
const [complaintRes, residentRes, weeklyServiceRes, sdgsRes] =
await Promise.all([
apiClient.GET("/api/complaint/stats"),
apiClient.GET("/api/resident/stats"),
apiClient.GET("/api/complaint/service-weekly"),
apiClient.GET("/api/dashboard/sdgs"),
],
);
]);
setStats({
complaints: (complaintRes.data as { data: typeof stats.complaints })
@@ -138,7 +139,9 @@ export function DashboardContent() {
{sdgsData.map((sdg) => (
<Grid.Col key={sdg.title} span={{ base: 9, md: 3 }}>
<SDGSCard
image={sdg.image ? <Image src={sdg.image} alt={sdg.title} /> : null}
image={
sdg.image ? <Image src={sdg.image} alt={sdg.title} /> : null
}
title={sdg.title}
score={sdg.score}
/>

View File

@@ -96,7 +96,13 @@ export function ChartAPBDes() {
</Bar>
</BarChart>
</ResponsiveContainer>
<Text size="sm" fw={600} w={40} ta="right" c={dark ? "white" : "gray.9"}>
<Text
size="sm"
fw={600}
w={40}
ta="right"
c={dark ? "white" : "gray.9"}
>
{item.value}%
</Text>
</Group>

View File

@@ -51,8 +51,14 @@ export function ChartSurat() {
console.log("📊 Service trends response:", res);
// Check if response has data
if (res.data?.data && Array.isArray(res.data.data) && res.data.data.length > 0) {
const chartData = (res.data.data as { month: string; count: number }[]).map((d) => ({
if (
res.data?.data &&
Array.isArray(res.data.data) &&
res.data.data.length > 0
) {
const chartData = (
res.data.data as { month: string; count: number }[]
).map((d) => ({
month: d.month,
value: Number(d.count),
}));

View File

@@ -1,9 +1,16 @@
import { Card, Group, Loader, Stack, Text, useMantineColorScheme } from "@mantine/core";
import {
Card,
Group,
Loader,
Stack,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { format } from "date-fns";
import { id } from "date-fns/locale";
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;

View File

@@ -1,4 +1,10 @@
import { Card, Group, Loader, Text, useMantineColorScheme } from "@mantine/core";
import {
Card,
Group,
Loader,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { useEffect, useState } from "react";
import {
Bar,

View File

@@ -47,10 +47,26 @@ export function ProgressChart() {
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" },
{
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);
}

View File

@@ -0,0 +1,176 @@
import {
Box,
Button,
Card,
Group,
Stack,
Text,
Title,
Alert,
Loader,
Badge,
Divider,
} from "@mantine/core";
import { IconRefresh, IconCheck, IconAlertCircle, IconClock } from "@tabler/icons-react";
import { useState, useEffect } from "react";
import { apiClient } from "@/utils/api-client";
import dayjs from "dayjs";
import "dayjs/locale/id";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
dayjs.locale("id");
const SinkronisasiSettings = () => {
const [loading, setLoading] = useState(false);
const [lastSync, setLastSync] = useState<string | null>(null);
const [status, setStatus] = useState<{
type: "success" | "error" | null;
message: string;
}>({ type: null, message: "" });
const fetchLastSync = async () => {
const { data } = await apiClient.GET("/api/noc/last-sync", {
params: { query: { idDesa: "darmasaba" } },
});
if (data?.lastSyncedAt) {
setLastSync(data.lastSyncedAt);
}
};
useEffect(() => {
fetchLastSync();
}, []);
const handleSync = async () => {
setLoading(true);
setStatus({ type: null, message: "" });
try {
const { data, error } = await apiClient.POST("/api/noc/sync");
if (error) {
setStatus({
type: "error",
message: (error as any).error || "Gagal melakukan sinkronisasi",
});
} else if (data?.success) {
setStatus({
type: "success",
message: data.message || "Sinkronisasi berhasil dilakukan",
});
if (data.lastSyncedAt) {
setLastSync(data.lastSyncedAt);
}
}
} catch (err) {
setStatus({
type: "error",
message: "Terjadi kesalahan sistem saat sinkronisasi",
});
} finally {
setLoading(false);
}
};
return (
<Box pr={"50%"}>
<Title order={2} mb="lg">
Sinkronisasi Data NOC
</Title>
<Text c="dimmed" mb="xl">
Gunakan fitur ini untuk memperbarui data dashboard dengan data terbaru dari
server Network Operation Center (NOC) darmasaba.muku.id.
</Text>
<Card withBorder padding="lg" radius="md" mb="xl">
<Stack gap="md">
<Group justify="space-between">
<Group>
<IconClock size={20} color="gray" />
<Text fw={500}>Status Terakhir</Text>
</Group>
<Badge color={lastSync ? "green" : "gray"} variant="light">
{lastSync ? "Terkoneksi" : "Belum Pernah Sinkron"}
</Badge>
</Group>
<Divider />
<Box>
<Text size="sm" c="dimmed">
Waktu Sinkronisasi Terakhir:
</Text>
<Text fw={700} size="lg">
{lastSync
? dayjs(lastSync).format("DD MMMM YYYY, HH:mm:ss")
: "Belum pernah dilakukan"}
</Text>
{lastSync && (
<Text size="xs" c="dimmed" mt={4}>
({dayjs(lastSync).fromNow()})
</Text>
)}
</Box>
{status.type && (
<Alert
icon={
status.type === "success" ? (
<IconCheck size={16} />
) : (
<IconAlertCircle size={16} />
)
}
title={status.type === "success" ? "Berhasil" : "Kesalahan"}
color={status.type === "success" ? "green" : "red"}
onClose={() => setStatus({ type: null, message: "" })}
withCloseButton
>
{status.message}
</Alert>
)}
<Button
leftSection={
loading ? <Loader size={16} color="white" /> : <IconRefresh size={16} />
}
onClick={handleSync}
loading={loading}
fullWidth
mt="md"
>
Sinkronkan Sekarang
</Button>
</Stack>
</Card>
<Title order={2} mb="lg">
Informasi API
</Title>
<Card withBorder padding="md" radius="md" bg="gray.0">
<Stack gap="xs">
<Group>
<Text fw={600} size="sm" w={100}>URL Sumber:</Text>
<Text size="sm" style={{ wordBreak: 'break-all' }}>https://darmasaba.muku.id/api/noc/</Text>
</Group>
<Group>
<Text fw={600} size="sm" w={100}>ID Desa:</Text>
<Text size="sm">darmasaba</Text>
</Group>
<Group>
<Text fw={600} size="sm" w={100}>Model Data:</Text>
<Badge size="xs" variant="outline">Divisi</Badge>
<Badge size="xs" variant="outline">Kegiatan</Badge>
<Badge size="xs" variant="outline">Event</Badge>
<Badge size="xs" variant="outline">Diskusi</Badge>
</Group>
</Stack>
</Card>
</Box>
);
};
export default SinkronisasiSettings;

View File

@@ -48,6 +48,7 @@ export function Sidebar({ className }: SidebarProps) {
{ name: "Notifikasi", path: "/pengaturan/notifikasi" },
{ name: "Keamanan", path: "/pengaturan/keamanan" },
{ name: "Akses & Tim", path: "/pengaturan/akses-dan-tim" },
{ name: "Sinkronisasi NOC", path: "/pengaturan/sinkronisasi" },
];
// Check if any settings submenu is active