"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" interface CrimeSidebarProps { className?: string defaultCollapsed?: boolean selectedCategory?: string | "all" selectedYear?: number selectedMonth?: number | "all" } // Updated interface to match the structure returned by getCrimeByYearAndMonth interface ICrimesProps { id: string district_id: string districts: { name: string geographics?: { address: string land_area: number year: number }[] demographics?: { number_of_unemployed: number population: number population_density: number year: number }[] } number_of_crime: number level: $Enums.crime_rates score: number month: number year: number crime_incidents: Array<{ id: string timestamp: Date description: string status: string crime_categories: { name: string type: string } locations: { address: string latitude: number longitude: number } }> } export default function CrimeSidebar({ className, defaultCollapsed = true, selectedCategory = "all", selectedYear: propSelectedYear, selectedMonth: propSelectedMonth }: CrimeSidebarProps) { const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed) const [activeTab, setActiveTab] = useState("incidents") const [currentTime, setCurrentTime] = useState(new Date()) const [location, setLocation] = useState("Jember, East Java") const { availableYears, isYearsLoading, crimes, isCrimesLoading, crimesError, selectedYear: hookSelectedYear, selectedMonth: hookSelectedMonth, } = usePrefetchedCrimeData() // Use props for selectedYear and selectedMonth if provided, otherwise fall back to hook values const selectedYear = propSelectedYear || hookSelectedYear const selectedMonth = propSelectedMonth || hookSelectedMonth // Update current time every minute for the real-time display useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()) }, 60000) return () => clearInterval(timer) }, []) // Format date with selected year and month if provided const getDisplayDate = () => { // If we have a specific month selected, use that for display if (selectedMonth && selectedMonth !== 'all') { const date = new Date() date.setFullYear(selectedYear) date.setMonth(Number(selectedMonth) - 1) // Month is 0-indexed in JS Date return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long' }).format(date) } // Otherwise show today's 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 { data: categoriesData } = useGetCrimeCategories() const crimeStats = useMemo(() => { // Return default values if crimes is undefined, null, or not an array if (!crimes || !Array.isArray(crimes) || crimes.length === 0) return { todaysIncidents: 0, totalIncidents: 0, recentIncidents: [], categoryCounts: {}, districts: {}, incidentsByMonth: Array(12).fill(0), clearanceRate: 0 } // Make sure we have a valid array to work with let filteredCrimes = [...crimes] if (selectedCategory !== "all") { filteredCrimes = crimes.filter((crime: ICrimesProps) => crime.crime_incidents.some(incident => incident.crime_categories.name === selectedCategory ) ) } // Collect all incidents from all crimes const allIncidents = filteredCrimes.flatMap((crime: ICrimesProps) => 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 = allIncidents.length const today = new Date() const thirtyDaysAgo = new Date() thirtyDaysAgo.setDate(today.getDate() - 30) const recentIncidents = allIncidents .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 todaysIncidents = recentIncidents.filter((incident) => { const incidentDate = incident?.timestamp ? new Date(incident.timestamp) : new Date(0) return incidentDate.toDateString() === today.toDateString() }).length const categoryCounts = allIncidents.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: ICrimesProps) => { 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) allIncidents.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 = allIncidents.filter(incident => incident?.status?.toLowerCase() === "resolved" ).length const clearanceRate = totalIncidents > 0 ? Math.round((resolvedIncidents / totalIncidents) * 100) : 0 return { todaysIncidents, totalIncidents, recentIncidents: recentIncidents.slice(0, 10), categoryCounts, districts, incidentsByMonth, clearanceRate } }, [crimes, selectedCategory]) // Generate a time period display for the current view 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" } return (
{/* Header with improved styling */}
Crime Analysis {!isCrimesLoading && ( {getTimePeriodDisplay()} )}
{/* Improved tabs with pill style */} Dashboard Statistics Information
{isCrimesLoading ? (
) : ( <> {/* Enhanced info card */}
{formattedDate}
{formattedTime}
{location}
{crimeStats.totalIncidents || 0} incidents reported {selectedMonth !== 'all' ? ` in ${getMonthName(Number(selectedMonth))}` : ` in ${selectedYear}`}
{/* Enhanced stat cards */}
} 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" />
} > {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) => ( ))}
)}
)}
{isCrimesLoading ? (
) : ( <> 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" } function IncidentCard({ title, time, location, severity }: 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}
{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}%
) }