"use client" import React, { useState, useEffect, useMemo } from "react" import { AlertTriangle, BarChart, ChevronRight, MapPin, Skull, Shield, FileText, Clock, Calendar, MapIcon, Info, CheckCircle, AlertCircle, XCircle, Bell, Users, Search, List, RefreshCw, Eye, X, ChevronLeft, PieChart, LineChart, Activity, Filter, Layers, Settings, Menu } from 'lucide-react' import { Separator } from "@/app/_components/ui/separator" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/app/_components/ui/card" import { cn } from "@/app/_lib/utils" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs" import { Badge } from "@/app/_components/ui/badge" import { Button } from "@/app/_components/ui/button" import { CRIME_RATE_COLORS } from "@/app/_utils/const/map" import { usePrefetchedCrimeData } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-prefetch-crimes" import { getMonthName, formatDateString } from "@/app/_utils/common" import { Skeleton } from "@/app/_components/ui/skeleton" import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries" import { $Enums } from "@prisma/client" import { useMap } from "react-map-gl/mapbox" import { ICrimes } from "@/app/_utils/types/crimes" interface CrimeSidebarProps { className?: string defaultCollapsed?: boolean selectedCategory?: string | "all" selectedYear: number selectedMonth?: number | "all" crimes: ICrimes[] isLoading?: boolean } export default function CrimeSidebar({ className, defaultCollapsed = true, selectedCategory = "all", selectedYear, selectedMonth, crimes = [], isLoading = false, }: CrimeSidebarProps) { const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed) const [activeTab, setActiveTab] = useState("incidents") const [activeIncidentTab, setActiveIncidentTab] = useState("recent") const [currentTime, setCurrentTime] = useState(new Date()) const [location, setLocation] = useState("Jember, East Java") const [paginationState, setPaginationState] = useState>({}) const { current: map } = useMap() useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()) }, 60000) return () => clearInterval(timer) }, []) const getDisplayDate = () => { if (selectedMonth && selectedMonth !== 'all') { const date = new Date() date.setFullYear(selectedYear) date.setMonth(Number(selectedMonth) - 1) return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long' }).format(date) } return new Intl.DateTimeFormat('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }).format(currentTime) } const formattedDate = getDisplayDate() const formattedTime = new Intl.DateTimeFormat('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }).format(currentTime) const crimeStats = useMemo(() => { if (!crimes || !Array.isArray(crimes) || crimes.length === 0) return { todaysIncidents: 0, totalIncidents: 0, recentIncidents: [], filteredIncidents: [], categoryCounts: {}, districts: {}, incidentsByMonth: Array(12).fill(0), clearanceRate: 0, incidentsByMonthDetail: {} as Record, availableMonths: [] as string[] } let filteredCrimes = [...crimes] const crimeIncidents = filteredCrimes.flatMap((crime: ICrimes) => crime.crime_incidents.map(incident => ({ id: incident.id, timestamp: incident.timestamp, description: incident.description, status: incident.status, category: incident.crime_categories.name, type: incident.crime_categories.type, address: incident.locations.address, latitude: incident.locations.latitude, longitude: incident.locations.longitude })) ) const totalIncidents = crimeIncidents.length const today = new Date() const thirtyDaysAgo = new Date() thirtyDaysAgo.setDate(today.getDate() - 30) const recentIncidents = crimeIncidents .filter((incident) => { if (!incident?.timestamp) return false const incidentDate = new Date(incident.timestamp) return incidentDate >= thirtyDaysAgo }) .sort((a, b) => { const bTime = b?.timestamp ? new Date(b.timestamp).getTime() : 0 const aTime = a?.timestamp ? new Date(a.timestamp).getTime() : 0 return bTime - aTime }) const filteredIncidents = crimeIncidents.sort((a, b) => { const bTime = b?.timestamp ? new Date(b.timestamp).getTime() : 0 const aTime = a?.timestamp ? new Date(a.timestamp).getTime() : 0 return bTime - aTime }) const todaysIncidents = recentIncidents.filter((incident) => { const incidentDate = incident?.timestamp ? new Date(incident.timestamp) : new Date(0) return incidentDate.toDateString() === today.toDateString() }).length const categoryCounts = crimeIncidents.reduce((acc: Record, incident) => { const category = incident?.category || 'Unknown' acc[category] = (acc[category] || 0) + 1 return acc }, {} as Record) const districts = filteredCrimes.reduce((acc: Record, crime: ICrimes) => { const districtName = crime.districts.name || 'Unknown' acc[districtName] = (acc[districtName] || 0) + (crime.number_of_crime || 0) return acc }, {} as Record) const incidentsByMonth = Array(12).fill(0) crimeIncidents.forEach((incident) => { if (!incident?.timestamp) return; const date = new Date(incident.timestamp) const month = date.getMonth() if (month >= 0 && month < 12) { incidentsByMonth[month]++ } }) const resolvedIncidents = crimeIncidents.filter(incident => incident?.status?.toLowerCase() === "resolved" ).length const clearanceRate = totalIncidents > 0 ? Math.round((resolvedIncidents / totalIncidents) * 100) : 0 const incidentsByMonthDetail: Record = {} const availableMonths: string[] = [] crimeIncidents.forEach(incident => { if (!incident?.timestamp) return const date = new Date(incident.timestamp) const monthKey = `${date.getFullYear()}-${date.getMonth() + 1}` if (!incidentsByMonthDetail[monthKey]) { incidentsByMonthDetail[monthKey] = [] availableMonths.push(monthKey) } incidentsByMonthDetail[monthKey].push(incident) }) Object.keys(incidentsByMonthDetail).forEach(monthKey => { incidentsByMonthDetail[monthKey].sort((a, b) => { const bTime = b?.timestamp ? new Date(b.timestamp).getTime() : 0 const aTime = a?.timestamp ? new Date(a.timestamp).getTime() : 0 return bTime - aTime }) }) availableMonths.sort((a, b) => { const [yearA, monthA] = a.split('-').map(Number) const [yearB, monthB] = b.split('-').map(Number) if (yearB !== yearA) return yearB - yearA return monthB - monthA }) return { todaysIncidents, totalIncidents, recentIncidents: recentIncidents.slice(0, 10), filteredIncidents, categoryCounts, districts, incidentsByMonth, clearanceRate, incidentsByMonthDetail, availableMonths } }, [crimes]) useEffect(() => { if (crimeStats.availableMonths && crimeStats.availableMonths.length > 0) { const initialState: Record = {} crimeStats.availableMonths.forEach(month => { initialState[month] = 0 }) setPaginationState(initialState) } }, [crimeStats.availableMonths]) const formatMonthKey = (monthKey: string): string => { const [year, month] = monthKey.split('-').map(Number) return `${getMonthName(month)} ${year}` } const handlePageChange = (monthKey: string, direction: 'next' | 'prev') => { setPaginationState(prev => { const currentPage = prev[monthKey] || 0 const totalPages = Math.ceil((crimeStats.incidentsByMonthDetail[monthKey]?.length || 0) / 5) if (direction === 'next' && currentPage < totalPages - 1) { return { ...prev, [monthKey]: currentPage + 1 } } else if (direction === 'prev' && currentPage > 0) { return { ...prev, [monthKey]: currentPage - 1 } } return prev }) } const getTimePeriodDisplay = () => { if (selectedMonth && selectedMonth !== 'all') { return `${getMonthName(Number(selectedMonth))} ${selectedYear}` } return `${selectedYear} - All months` } const topCategories = useMemo(() => { if (!crimeStats.categoryCounts) return [] return Object.entries(crimeStats.categoryCounts) .sort((a, b) => (b[1] as number) - (a[1] as number)) .slice(0, 4) .map(([type, count]) => { const percentage = Math.round(((count as number) / crimeStats.totalIncidents) * 100) || 0 return { type, count: count as number, percentage } }) }, [crimeStats]) const getTimeAgo = (timestamp: string | Date) => { const now = new Date() const eventTime = new Date(timestamp) const diffMs = now.getTime() - eventTime.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMins / 60) const diffDays = Math.floor(diffHours / 24) if (diffDays > 0) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago` if (diffHours > 0) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago` if (diffMins > 0) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago` return 'just now' } const getIncidentSeverity = (incident: any): "Low" | "Medium" | "High" | "Critical" => { if (!incident) return "Low"; const category = incident.category || "Unknown"; const highSeverityCategories = [ 'Pembunuhan', 'Perkosaan', 'Penculikan', 'Lahgun Senpi/Handak/Sajam', 'PTPPO', 'Trafficking In Person' ] const mediumSeverityCategories = [ 'Penganiayaan Berat', 'Penganiayaan Ringan', 'Pencurian Biasa', 'Curat', 'Curas', 'Curanmor', 'Pengeroyokan', 'PKDRT', 'Penggelapan', 'Pengrusakan' ] if (highSeverityCategories.includes(category)) return "High" if (mediumSeverityCategories.includes(category)) return "Medium" if (incident.type === "Pidana Tertentu") return "Medium" return "Low" } const handleIncidentClick = (incident: any) => { if (!map || !incident.longitude || !incident.latitude) return map.flyTo({ center: [incident.longitude, incident.latitude], zoom: 15, pitch: 60, bearing: 0, duration: 1500, easing: (t) => t * (2 - t), }) const customEvent = new CustomEvent("incident_click", { detail: incident, bubbles: true }) if (map.getMap().getCanvas()) { map.getMap().getCanvas().dispatchEvent(customEvent) } else { document.dispatchEvent(customEvent) } } return (
Crime Analysis {!isLoading && ( {getTimePeriodDisplay()} )}
Dashboard Statistics Information
{isLoading ? (
) : ( <>
{formattedDate}
{formattedTime}
{location}
{crimeStats.totalIncidents || 0} incidents reported {selectedMonth !== 'all' ? ` in ${getMonthName(Number(selectedMonth))}` : ` in ${selectedYear}`}
} statusColor="text-green-400" updatedTime={getTimePeriodDisplay()} bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" borderColor="border-sidebar-border" /> } statusColor="text-amber-400" updatedTime="Last 30 days" bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" borderColor="border-sidebar-border" /> 0 ? topCategories[0].type : "None"} statusIcon={} statusColor="text-green-400" bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" borderColor="border-sidebar-border" /> } statusColor="text-purple-400" updatedTime="Affected areas" bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" borderColor="border-sidebar-border" />

Incident Reports

Recent History
{crimeStats.recentIncidents.length === 0 ? (

{selectedCategory !== "all" ? `No ${selectedCategory} incidents found` : "No recent incidents reported"}

Try adjusting your filters or checking back later

) : (
{crimeStats.recentIncidents.slice(0, 6).map((incident) => ( handleIncidentClick(incident)} /> ))}
)}
{crimeStats.availableMonths && crimeStats.availableMonths.length === 0 ? (

{selectedCategory !== "all" ? `No ${selectedCategory} incidents found in the selected period` : "No incidents found in the selected period"}

Try adjusting your filters

) : (
Showing incidents from {crimeStats.availableMonths.length} {crimeStats.availableMonths.length === 1 ? 'month' : 'months'} {selectedCategory !== "all" ? selectedCategory : "All Categories"}
{crimeStats.availableMonths.map(monthKey => { const incidents = crimeStats.incidentsByMonthDetail[monthKey] || [] const pageSize = 5 const currentPage = paginationState[monthKey] || 0 const totalPages = Math.ceil(incidents.length / pageSize) const startIdx = currentPage * pageSize const endIdx = startIdx + pageSize const paginatedIncidents = incidents.slice(startIdx, endIdx) if (incidents.length === 0) return null return (

{formatMonthKey(monthKey)}

{incidents.length} incident{incidents.length !== 1 ? 's' : ''}
{paginatedIncidents.map((incident) => ( handleIncidentClick(incident)} showTimeAgo={false} /> ))}
{totalPages > 1 && (
Page {currentPage + 1} of {totalPages}
)}
) })}
)}
)}
{isLoading ? (
) : ( <> Monthly Incidents {selectedYear}
{crimeStats.incidentsByMonth.map((count, i) => { const maxCount = Math.max(...crimeStats.incidentsByMonth) const height = maxCount > 0 ? (count / maxCount) * 100 : 0 return (
) })}
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
}>
} bgColor="bg-gradient-to-r from-blue-900/30 to-blue-800/20" /> c > 0).length || 1) ).toString()} change={selectedMonth !== 'all' ? `in ${getMonthName(Number(selectedMonth))}` : "per active month"} isPositive={false} icon={} bgColor="bg-gradient-to-r from-amber-900/30 to-amber-800/20" /> 50} icon={} bgColor="bg-gradient-to-r from-green-900/30 to-green-800/20" />
}>
{topCategories.length > 0 ? ( topCategories.map((category) => ( )) ) : (

No crime data available

Try selecting a different time period

)}
)} }>

Crime Severity

Low Crime Rate
Medium Crime Rate
High Crime Rate

Map Markers

Individual Incident
5
Incident Cluster
}>

SIGAP Crime Map provides real-time visualization and analysis of crime incidents across Jember region.

Data is sourced from official police reports and updated daily to ensure accurate information.

Version 1.2.4
Last Updated June 18, 2024
}>
Filtering

Use the year, month, and category filters at the top to refine the data shown on the map.

District Information

Click on any district to view detailed crime statistics for that area.

Incidents

Click on incident markers to view details about specific crime reports.

) } interface SidebarSectionProps { title: string children: React.ReactNode icon?: React.ReactNode } function SidebarSection({ title, children, icon }: SidebarSectionProps) { return (

{icon} {title}

{children}
) } interface SystemStatusCardProps { title: string status: string statusIcon: React.ReactNode statusColor: string updatedTime?: string bgColor?: string borderColor?: string } function SystemStatusCard({ title, status, statusIcon, statusColor, updatedTime, bgColor = "bg-sidebar-accent/20", borderColor = "border-sidebar-border" }: SystemStatusCardProps) { return (
{title}
{statusIcon} {status}
{updatedTime && (
{updatedTime}
)}
); } interface EnhancedIncidentCardProps { title: string time: string location: string severity: "Low" | "Medium" | "High" | "Critical" onClick?: () => void showTimeAgo?: boolean } function IncidentCard({ title, time, location, severity, onClick, showTimeAgo = true }: EnhancedIncidentCardProps) { const getBadgeColor = () => { switch (severity) { case "Low": return "bg-green-500/20 text-green-300"; case "Medium": return "bg-yellow-500/20 text-yellow-300"; case "High": return "bg-orange-500/20 text-orange-300"; case "Critical": return "bg-red-500/20 text-red-300"; default: return "bg-gray-500/20 text-gray-300"; } }; const getBorderColor = () => { switch (severity) { case "Low": return "border-l-green-500"; case "Medium": return "border-l-yellow-500"; case "High": return "border-l-orange-500"; case "Critical": return "border-l-red-500"; default: return "border-l-gray-500"; } }; return (

{title}

{severity}
{location}
{showTimeAgo ? ( time ) : ( <> {time} )}
); } interface StatCardProps { title: string value: string change: string isPositive?: boolean icon?: React.ReactNode bgColor?: string } function StatCard({ title, value, change, isPositive = false, icon, bgColor = "bg-white/10" }: StatCardProps) { return (
{icon} {title} {change}
{value}
) } interface CrimeTypeCardProps { type: string count: number percentage: number } function CrimeTypeCard({ type, count, percentage }: CrimeTypeCardProps) { return (
{type} {count} cases
{percentage}%
) }