feat(noc): implement sync management UI and backend integration
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
176
src/components/pengaturan/sinkronisasi.tsx
Normal file
176
src/components/pengaturan/sinkronisasi.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user