feat: tambah peak hours chart di halaman village detail
This commit is contained in:
@@ -33,6 +33,11 @@ export const API_URLS = {
|
||||
return `${DESA_PLUS_PROXY}/api/monitoring/log-all-villages?${params}`
|
||||
},
|
||||
getStaleVillages: (days: 7 | 14 | 30 = 7) => `${DESA_PLUS_PROXY}/api/monitoring/stale-villages?days=${days}`,
|
||||
getPeakHours: (idVillage?: string) => {
|
||||
const params = new URLSearchParams()
|
||||
if (idVillage) params.set('idVillage', idVillage)
|
||||
return `${DESA_PLUS_PROXY}/api/monitoring/peak-hours?${params}`
|
||||
},
|
||||
getInactiveUsers: (days: 7 | 14 | 30 = 7, idVillage?: string, page = 1) => {
|
||||
const params = new URLSearchParams({ days: String(days), page: String(page) })
|
||||
if (idVillage) params.set('idVillage', idVillage)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AreaChart } from '@mantine/charts'
|
||||
import { AreaChart, BarChart } from '@mantine/charts'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
@@ -194,6 +194,73 @@ function ActivityChart({ villageId }: { villageId: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Peak Hours Chart ──────────────────────────────────────────────────────────
|
||||
|
||||
function PeakHoursChart({ villageId }: { villageId: string }) {
|
||||
const { data: response, isLoading } = useSWR(API_URLS.getPeakHours(villageId), fetcher)
|
||||
const hours: { hour: number; label: string; count: number }[] = response?.data?.hours || []
|
||||
const peak: { label: string; count: number } | null = response?.data?.peak || null
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="xl" p="lg">
|
||||
<Group justify="space-between" mb="lg" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size={28} radius="md" variant="light" color="violet">
|
||||
<TbClock size={14} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={0}>
|
||||
<Text fw={700} size="sm">Peak Activity Hours</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{peak && peak.count > 0
|
||||
? `Busiest hour: ${peak.label} (${peak.count.toLocaleString()} activities)`
|
||||
: 'No activity data'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
<Stack h={200} align="center" justify="center">
|
||||
<Loader type="dots" />
|
||||
</Stack>
|
||||
) : (
|
||||
<BarChart
|
||||
h={200}
|
||||
data={hours}
|
||||
dataKey="label"
|
||||
series={[{ name: 'count', color: 'violet.5' }]}
|
||||
withTooltip
|
||||
withXAxis
|
||||
withYAxis={false}
|
||||
tickLine="none"
|
||||
gridAxis="none"
|
||||
barProps={{ radius: 4 }}
|
||||
tooltipProps={{
|
||||
content: ({ active, payload, label }: any) => {
|
||||
if (!active || !payload?.length) return null
|
||||
return (
|
||||
<div style={{
|
||||
backgroundColor: '#1A1B1E',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #373A40',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<div style={{ fontSize: '12px', fontWeight: 600, color: '#C1C2C5', marginBottom: '4px' }}>{label}</div>
|
||||
<div style={{ fontSize: '11px', color: '#9775FA' }}>
|
||||
Activities: <span style={{ fontWeight: 700 }}>{payload[0].value}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Recent Activity Logs ──────────────────────────────────────────────────────
|
||||
|
||||
function RecentVillageLogs({ villageId }: { villageId: string }) {
|
||||
@@ -563,6 +630,9 @@ function VillageDetailPage() {
|
||||
{/* ── Activity Chart ── */}
|
||||
<ActivityChart villageId={villageId} />
|
||||
|
||||
{/* ── Peak Hours Chart ── */}
|
||||
<PeakHoursChart villageId={villageId} />
|
||||
|
||||
{/* ── Recent Logs + System Info ── */}
|
||||
<Grid gutter="md" align="flex-start">
|
||||
<Grid.Col span={{ base: 12, md: 8 }}>
|
||||
|
||||
Reference in New Issue
Block a user