diff --git a/src/app/darmasaba/(pages)/keamanan/cctv/_com/CctvMapSection.tsx b/src/app/darmasaba/(pages)/keamanan/cctv/_com/CctvMapSection.tsx new file mode 100644 index 00000000..2d6b7c13 --- /dev/null +++ b/src/app/darmasaba/(pages)/keamanan/cctv/_com/CctvMapSection.tsx @@ -0,0 +1,79 @@ +'use client' +import { CctvData } from '@/app/admin/(dashboard)/_state/keamanan/cctv'; +import L from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'; + +type Props = { + data: CctvData[]; +}; + +function createCctvIcon(status: 'Online' | 'Offline') { + const color = status === 'Online' ? '#2f9e44' : '#e03131'; + return L.divIcon({ + className: '', + html: `
`, + iconSize: [18, 18], + iconAnchor: [9, 9], + popupAnchor: [0, -12], + }); +} + +export default function CctvMapSection({ data }: Props) { + const markers = data.filter((c) => c.latitude != null && c.longitude != null); + if (markers.length === 0) return null; + + const center: [number, number] = [ + markers.reduce((s, m) => s + m.latitude!, 0) / markers.length, + markers.reduce((s, m) => s + m.longitude!, 0) / markers.length, + ]; + + return ( + + + {markers.map((cctv) => ( + + +
+
+ {cctv.kode} + + {cctv.status} + +
+
{cctv.nama}
+
{cctv.lokasi}
+
+
+
+ ))} +
+ ); +} diff --git a/src/app/darmasaba/(pages)/keamanan/cctv/page.tsx b/src/app/darmasaba/(pages)/keamanan/cctv/page.tsx new file mode 100644 index 00000000..951fcbc7 --- /dev/null +++ b/src/app/darmasaba/(pages)/keamanan/cctv/page.tsx @@ -0,0 +1,239 @@ +'use client' +import cctvState, { CctvData } from '@/app/admin/(dashboard)/_state/keamanan/cctv'; +import colors from '@/con/colors'; +import { + Badge, + Box, + Center, + Grid, + GridCol, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Text, + TextInput, + Title, +} from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconCamera, IconClock, IconMapPin, IconSearch, IconVideo } from '@tabler/icons-react'; +import dynamic from 'next/dynamic'; +import { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import BackButton from '../../desa/layanan/_com/BackButto'; + +const CctvMapSection = dynamic(() => import('./_com/CctvMapSection'), { ssr: false }); + +function Page() { + const state = useProxy(cctvState); + const [search, setSearch] = useState(''); + const [debouncedSearch] = useDebouncedValue(search, 500); + const [allCctv, setAllCctv] = useState([]); + const [mapLoading, setMapLoading] = useState(true); + + const { data, page, totalPages, loading } = state.findMany; + + useShallowEffect(() => { + cctvState.findMany.search = debouncedSearch; + cctvState.findMany.limit = 9; + cctvState.findMany.load(); + }, [page, debouncedSearch]); + + useEffect(() => { + fetch('/api/keamanan/cctv/find-many?limit=200&page=1') + .then((r) => r.json()) + .then((res) => { + if (res.success) setAllCctv(res.data ?? []); + }) + .catch(() => {}) + .finally(() => setMapLoading(false)); + }, []); + + const markersCount = allCctv.filter((c) => c.latitude != null && c.longitude != null).length; + const onlineCount = allCctv.filter((c) => c.status === 'Online').length; + const offlineCount = allCctv.filter((c) => c.status === 'Offline').length; + + if (loading || !data) { + return ( + + + + ); + } + + return ( + + + + + + {/* Header */} + + + + + CCTV Keamanan Desa + + + + setSearch(e.target.value)} + leftSection={} + w="100%" + /> + + + + Informasi titik-titik CCTV yang terpasang di wilayah Desa Darmasaba untuk mendukung + keamanan dan ketertiban lingkungan. + + + + {/* Map Section */} + + + {/* Map Legend */} + + + + + Peta Titik CCTV + + {markersCount > 0 && ( + + ({markersCount} titik terpetakan) + + )} + + + + + Online ({onlineCount}) + + + + Offline ({offlineCount}) + + + + + {/* Map */} + + {mapLoading ? ( + + ) : markersCount === 0 ? ( +
+ + + Belum ada titik koordinat CCTV + +
+ ) : ( + + )} +
+
+
+ + {/* Card Grid */} + + {data.length === 0 ? ( +
+ + + Tidak ada data CCTV ditemukan + +
+ ) : ( + + {data.map((item) => ( + + + + + + + {item.kode} + + + + {item.status} + + + + + {item.nama} + + + + + + {item.lokasi} + + + + + + + {new Date(item.lastActive).toLocaleString('id-ID', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + + + + + ))} + + )} +
+ +
+ { + cctvState.findMany.page = newPage; + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + size="lg" + radius="xl" + styles={{ + control: { + border: `1px solid ${colors['blue-button']}`, + }, + }} + /> +
+
+ ); +} + +export default Page; diff --git a/src/con/navbar-list-menu.ts b/src/con/navbar-list-menu.ts index f59ccfa5..428820f1 100644 --- a/src/con/navbar-list-menu.ts +++ b/src/con/navbar-list-menu.ts @@ -168,6 +168,11 @@ const navbarListMenu = [ id: "4.6", name: "Tips Keamanan", href: "/darmasaba/keamanan/tips-keamanan" + }, + { + id: "4.7", + name: "CCTV", + href: "/darmasaba/keamanan/cctv" } ] },