From 2ab60befd8e176d2be4dc69da3a8c30d36bc1d2b Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Thu, 15 May 2025 14:15:18 +0700 Subject: [PATCH] feat: Enhance Crime Sidebar and Incident Tab with Recent Incidents - Updated CrimeSidebar to accept recent incidents as a prop for user reports from the last 24 hours. - Modified SidebarIncidentsTab to format and display recent incidents, including filtering by category and sorting by timestamp. - Adjusted CrimeMap to manage the selected year based on the source type and to handle recent incidents data. - Renamed MapLegend to ClusterLegend for clarity and updated its props accordingly. --- .../left/sidebar/components/incident-card.tsx | 299 ++++++++-- .../map/controls/left/sidebar/map-sidebar.tsx | 6 +- .../left/sidebar/tabs/incidents-tab.tsx | 551 ++++++++++++------ .../app/_components/map/crime-map.tsx | 330 ++++++----- .../_components/map/legends/map-legend.tsx | 4 +- 5 files changed, 819 insertions(+), 371 deletions(-) diff --git a/sigap-website/app/_components/map/controls/left/sidebar/components/incident-card.tsx b/sigap-website/app/_components/map/controls/left/sidebar/components/incident-card.tsx index 146a2e3..34e028e 100644 --- a/sigap-website/app/_components/map/controls/left/sidebar/components/incident-card.tsx +++ b/sigap-website/app/_components/map/controls/left/sidebar/components/incident-card.tsx @@ -1,75 +1,288 @@ import React from 'react' -import { AlertTriangle, MapPin, Calendar } from 'lucide-react' +import { Info, Clock, MapPin, AlertTriangle, ChevronRight, AlertCircle, Shield } from 'lucide-react' +import { Card, CardContent, CardHeader, CardFooter } from "@/app/_components/ui/card" import { Badge } from "@/app/_components/ui/badge" -import { Card, CardContent } from "@/app/_components/ui/card" +import { cn } from "@/app/_lib/utils" interface IIncidentCardProps { title: string - time: string location: string - severity: "Low" | "Medium" | "High" | "Critical" + time: string + severity?: number | "Low" | "Medium" | "High" | "Critical" onClick?: () => void + className?: string showTimeAgo?: boolean + status?: string | true + isUserReport?: boolean } export function IncidentCard({ title, - time, location, - severity, + time, + severity = 1, onClick, - showTimeAgo = true + className, + showTimeAgo = true, + status, + isUserReport = false }: IIncidentCardProps) { - 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"; + // Helper to normalize severity to a number + const normalizeSeverity = (sev: number | "Low" | "Medium" | "High" | "Critical"): number => { + if (typeof sev === "number") return sev; + switch (sev) { + case "Critical": + return 4; + case "High": + return 3; + case "Medium": + return 2; + case "Low": + default: + return 1; } }; - const getBorderColor = () => { + const getSeverityColor = (severity: number) => { 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"; + case 4: + return "border-purple-600 bg-purple-50 dark:bg-purple-950/30" + case 3: + return "border-red-500 bg-red-50 dark:bg-red-950/30" + case 2: + return "border-yellow-400 bg-yellow-50 dark:bg-yellow-950/30" + case 1: + default: + return "border-blue-500 bg-blue-50 dark:bg-blue-950/30" } - }; + } + + const getSeverityText = (severity: number) => { + switch (severity) { + case 4: + return "text-purple-600 dark:text-purple-400" + case 3: + return "text-red-500 dark:text-red-400" + case 2: + return "text-yellow-600 dark:text-yellow-400" + case 1: + default: + return "text-blue-500 dark:text-blue-400" + } + } + + const getSeverityLabel = (severity: number) => { + switch (severity) { + case 4: + return "Critical" + case 3: + return "High" + case 2: + return "Medium" + case 1: + default: + return "Low" + } + } + + const getStatusColor = (status: string) => { + switch (status?.toLowerCase()) { + case "resolved": + return "bg-green-100 text-green-700 border-green-300 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800" + case "pending": + default: + return "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800" + } + } + + const normalizedSeverity = normalizeSeverity(severity); return ( - -
- -
-
-

{title}

- {severity} -
-
- - {location} -
-
- {showTimeAgo ? ( - time - ) : ( - <> - - {time} - + +
+
+
+ + {getSeverityLabel(normalizedSeverity)} + + {isUserReport && ( + + User Report + )}
+ +

{title}

+ +
+
+ + {time} +
+ +
+ + {location} +
+
+ + {/* Add status badges for user reports */} + {isUserReport && typeof status === "string" && ( + + {status.charAt(0).toUpperCase() + status.slice(1)} + + )} +
+ +
+ +
+
+
+ + ) +} + +export function IncidentCardV2({ + title, + location, + time, + severity = 1, + onClick, + className, + showTimeAgo = true, + status, + isUserReport = false, +}: IIncidentCardProps) { + // Helper to normalize severity to a number + const normalizeSeverity = (sev: number | "Low" | "Medium" | "High" | "Critical"): number => { + if (typeof sev === "number") return sev + switch (sev) { + case "Critical": + return 4 + case "High": + return 3 + case "Medium": + return 2 + case "Low": + default: + return 1 + } + } + + const getSeverityGradient = (severity: number) => { + switch (severity) { + case 4: + return "bg-gradient-to-r from-purple-500/10 to-purple-600/5 dark:from-purple-900/20 dark:to-purple-800/10" + case 3: + return "bg-gradient-to-r from-red-500/10 to-red-600/5 dark:from-red-900/20 dark:to-red-800/10" + case 2: + return "bg-gradient-to-r from-yellow-500/10 to-yellow-600/5 dark:from-yellow-900/20 dark:to-yellow-800/10" + case 1: + default: + return "bg-gradient-to-r from-blue-500/10 to-blue-600/5 dark:from-blue-900/20 dark:to-blue-800/10" + } + } + + const getSeverityIconColor = (severity: number) => { + switch (severity) { + case 4: + return "text-purple-600 dark:text-purple-400" + case 3: + return "text-red-500 dark:text-red-400" + case 2: + return "text-yellow-600 dark:text-yellow-400" + case 1: + default: + return "text-blue-500 dark:text-blue-400" + } + } + + const getSeverityIcon = (severity: number) => { + switch (severity) { + case 4: + case 3: + return + case 2: + case 1: + default: + return + } + } + + const getStatusColor = (status: string) => { + switch (status?.toLowerCase()) { + case "resolved": + return "bg-green-100 text-green-700 border-green-300 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800" + case "pending": + default: + return "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800" + } + } + + const normalizedSeverity = normalizeSeverity(severity) + + return ( + + +
+ {/*
{getSeverityIcon(normalizedSeverity)}
*/} + +
+

{title}

+ +
+
+ + {time} +
+ +
+ + {location} +
+
+ + {/* Status badges */} +
+ {isUserReport && ( + + User Report + + )} + + {isUserReport && typeof status === "string" && ( + + {status.charAt(0).toUpperCase() + status.slice(1)} + + )} +
+ +
+ +
- ); + ) } diff --git a/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx b/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx index 8ec1d5c..a626130 100644 --- a/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx +++ b/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx @@ -25,7 +25,7 @@ import { import { Button } from "@/app/_components/ui/button"; import { Skeleton } from "@/app/_components/ui/skeleton"; import { useMap } from "react-map-gl/mapbox"; -import { ICrimes } from "@/app/_utils/types/crimes"; +import { ICrimes, IIncidentLogs } from "@/app/_utils/types/crimes"; // Import sidebar components import { SidebarIncidentsTab } from "./tabs/incidents-tab"; @@ -43,6 +43,7 @@ interface CrimeSidebarProps { selectedYear: number | "all"; selectedMonth?: number | "all"; crimes: ICrimes[]; + recentIncidents?: IIncidentLogs[]; // User reports from last 24 hours isLoading?: boolean; sourceType?: string; } @@ -54,6 +55,7 @@ export default function CrimeSidebar({ selectedYear, selectedMonth, crimes = [], + recentIncidents = [], // User reports from last 24 hours isLoading = false, sourceType = "cbt", }: CrimeSidebarProps) { @@ -244,7 +246,7 @@ export default function CrimeSidebar({ activeIncidentTab={activeIncidentTab} setActiveIncidentTab={setActiveIncidentTab} sourceType={sourceType} - // setActiveTab={setActiveTab} // Pass setActiveTab function + recentIncidents={recentIncidents} // Pass the recentIncidents /> diff --git a/sigap-website/app/_components/map/controls/left/sidebar/tabs/incidents-tab.tsx b/sigap-website/app/_components/map/controls/left/sidebar/tabs/incidents-tab.tsx index 1d37414..0ce9031 100644 --- a/sigap-website/app/_components/map/controls/left/sidebar/tabs/incidents-tab.tsx +++ b/sigap-website/app/_components/map/controls/left/sidebar/tabs/incidents-tab.tsx @@ -1,22 +1,45 @@ -import React from 'react' -import { AlertTriangle, AlertCircle, Clock, Shield, MapPin, ChevronLeft, ChevronRight, FileText, Calendar, ArrowRight, RefreshCw } from 'lucide-react' -import { Card, CardContent } from "@/app/_components/ui/card" -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 { formatMonthKey, getIncidentSeverity, getMonthName, getTimeAgo } from "@/app/_utils/common" -import { SystemStatusCard } from "../components/system-status-card" -import { IncidentCard } from "../components/incident-card" -import { ICrimeAnalytics } from '@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-crime-analytics' +import React from "react"; +import { + AlertCircle, + AlertTriangle, + ArrowRight, + Calendar, + ChevronLeft, + ChevronRight, + Clock, + FileText, + MapPin, + RefreshCw, + Shield, +} from "lucide-react"; +import { Card, CardContent } from "@/app/_components/ui/card"; +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 { + formatMonthKey, + getIncidentSeverity, + getMonthName, + getTimeAgo, +} from "@/app/_utils/common"; +import { SystemStatusCard } from "../components/system-status-card"; +import { IncidentCard, IncidentCardV2 } from "../components/incident-card"; +import { ICrimeAnalytics } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-crime-analytics"; +import { IIncidentLogs } from "@/app/_utils/types/crimes"; interface Incident { id: string; category: string; address: string; - timestamp: string; + timestamp: string | Date; // Accept both string and Date for timestamp district?: string; - severity?: number; - status?: string; + severity?: number | "Low" | "Medium" | "High" | "Critical"; // Match severity types + status?: string | true; // Match status types description?: string; location?: { lat: number; @@ -34,16 +57,11 @@ interface SidebarIncidentsTabProps { selectedCategory: string | "all"; getTimePeriodDisplay: () => string; paginationState: Record; - handlePageChange: (monthKey: string, direction: 'next' | 'prev') => void; + handlePageChange: (monthKey: string, direction: "next" | "prev") => void; handleIncidentClick: (incident: Incident) => void; activeIncidentTab: string; setActiveIncidentTab: (tab: string) => void; -} - -interface CrimeCategory { - type: string; - count: number; - percentage: number; + recentIncidents?: IIncidentLogs[]; // User reports from last 24 hours } export function SidebarIncidentsTab({ @@ -60,16 +78,58 @@ export function SidebarIncidentsTab({ handleIncidentClick, activeIncidentTab, setActiveIncidentTab, - sourceType = "cbt" + sourceType = "cbt", + recentIncidents = [], }: SidebarIncidentsTabProps & { sourceType?: string }) { - const topCategories = crimeStats.categoryCounts ? - Object.entries(crimeStats.categoryCounts) + const currentYear = new Date().getFullYear(); + const isCurrentYear = selectedYear === currentYear || + selectedYear === "all"; + + const topCategories = crimeStats.categoryCounts + ? Object.entries(crimeStats.categoryCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 4) .map(([type, count]) => { - const percentage = Math.round((count / crimeStats.totalIncidents) * 100) || 0 - return { type, count, percentage } - }) : [] + const percentage = + Math.round((count / crimeStats.totalIncidents) * 100) || 0; + return { type, count, percentage }; + }) + : []; + + // Filter history incidents by the selected year + const filteredAvailableMonths = crimeStats.availableMonths.filter( + (monthKey) => { + // If selectedYear is "all", show all months + if (selectedYear === "all") return true; + + // Extract year from the monthKey (format: YYYY-MM) + const yearFromKey = parseInt(monthKey.split("-")[0]); + return yearFromKey === selectedYear; + }, + ); + + // Format recent incidents data from user reports + const formattedRecentIncidents = recentIncidents + .filter((incident) => { + // Filter by category if needed + return selectedCategory === "all" || + incident.category?.toLowerCase() === + selectedCategory.toLowerCase(); + }) + .map((incident) => ({ + id: incident.id, + category: incident.category || "Uncategorized", + address: incident.address || "Unknown location", + timestamp: incident.timestamp + ? incident.timestamp.toString() + : new Date().toISOString(), // Convert to string + description: incident.description || "", + status: incident.verified || "pending", + severity: getIncidentSeverity(incident), + })) + .sort((a, b) => + new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() + ); // If source type is CBU, display warning instead of regular content if (sourceType === "cbu") { @@ -80,26 +140,39 @@ export function SidebarIncidentsTab({
-

Limited Data View

+

+ Limited Data View +

- The CBU data source only provides aggregated statistics without detailed incident information. + The CBU data source only provides aggregated statistics + without detailed incident information.

- Current Data Source: - CBU + + Current Data Source: + + + CBU +
- Recommended: - CBT + + Recommended: + + + CBT +

- To view detailed incident reports, individual crime records, and location-specific information, please switch to the CBT data source. + To view detailed incident reports, individual crime + records, and location-specific information, please + switch to the CBT data source.

@@ -115,7 +188,9 @@ export function SidebarIncidentsTab({

- The CBU (Crime By Unit) data provides insights at the district level, while CBT (Crime By Type) includes detailed incident-level information. + The CBU (Crime By Unit) data provides insights at + the district level, while CBT (Crime By Type) + includes detailed incident-level information.

@@ -128,8 +203,10 @@ export function SidebarIncidentsTab({ {/* Enhanced info card */} -
-
+
+
+
+
@@ -143,13 +220,19 @@ export function SidebarIncidentsTab({
- {location} + + {location} +
- {crimeStats.totalIncidents || 0} incidents reported - {selectedMonth !== 'all' ? ` in ${getMonthName(Number(selectedMonth))}` : ` in ${selectedYear}`} + {crimeStats.totalIncidents || 0} + {" "} + incidents reported + {selectedMonth !== "all" + ? ` in ${getMonthName(Number(selectedMonth))}` + : ` in ${selectedYear}`}
@@ -160,7 +243,9 @@ export function SidebarIncidentsTab({ } + statusIcon={ + + } statusColor="text-green-400" updatedTime={getTimePeriodDisplay()} bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" @@ -177,7 +262,9 @@ export function SidebarIncidentsTab({ /> 0 ? topCategories[0].type : "None"} + status={topCategories.length > 0 + ? topCategories[0].type + : "None"} statusIcon={} statusColor="text-green-400" bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" @@ -224,136 +311,274 @@ export function SidebarIncidentsTab({ {/* Recent Incidents Tab Content */} - {crimeStats.recentIncidents.length === 0 ? ( - - -
- -

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

+ +
+

+ Year Selection Notice +

+

+ Recent incidents are only available for + the current year ({currentYear}).

-

Try adjusting your filters or checking back later

+ + + + ) + : formattedRecentIncidents.length === 0 + ? ( + + +
+ +

+ {selectedCategory !== "all" + ? `No ${selectedCategory} incidents reported in the last 24 hours` + : "No incidents reported in the last 24 hours"} +

+

+ The most recent user reports will + appear here +

+
+
+
+ ) + : ( +
+ {formattedRecentIncidents.slice(0, 6).map(( + incident, + ) => ( + + handleIncidentClick( + incident as Incident, + )} + status={incident.status} + isUserReport={true} + /> + ))}
- - - ) : ( -
- {crimeStats.recentIncidents.slice(0, 6).map((incident: Incident) => ( - handleIncidentClick(incident)} - /> - ))} -
- )} + )} {/* History Incidents Tab Content */} - {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: string) => { - 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: Incident) => ( - handleIncidentClick(incident)} - showTimeAgo={false} - /> - ))} -
- - {totalPages > 1 && ( -
- - Page {currentPage + 1} of {totalPages} - -
- - -
-
- )} + {filteredAvailableMonths.length === 0 + ? ( + + +
+ +

+ {selectedCategory !== "all" + ? `No ${selectedCategory} incidents found in ${selectedYear === "all" + ? "any period" + : selectedYear + }` + : `No incidents found in ${selectedYear === "all" + ? "any period" + : selectedYear + }`} +

+

+ Try adjusting your filters +

- ) - })} -
- )} + + + ) + : ( +
+
+ + Showing incidents from{" "} + {filteredAvailableMonths.length}{" "} + {filteredAvailableMonths.length === 1 + ? "month" + : "months"} + {selectedYear !== "all" + ? ` in ${selectedYear}` + : ""} + + + {selectedCategory !== "all" + ? selectedCategory + : "All Categories"} + +
+ + {filteredAvailableMonths.map( + (monthKey: string) => { + 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: Incident, + ) => ( + + handleIncidentClick( + incident, + )} + showTimeAgo={false} + /> + ))} +
+ + {totalPages > 1 && ( +
+ + Page{" "} + {currentPage + 1} of + {" "} + {totalPages} + +
+ + +
+
+ )} +
+ ); + }, + )} +
+ )} - ) + ); } diff --git a/sigap-website/app/_components/map/crime-map.tsx b/sigap-website/app/_components/map/crime-map.tsx index 70e1b03..0282161 100644 --- a/sigap-website/app/_components/map/crime-map.tsx +++ b/sigap-website/app/_components/map/crime-map.tsx @@ -14,7 +14,7 @@ import { getMonthName } from "@/app/_utils/common"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useFullscreen } from "@/app/_hooks/use-fullscreen"; import { Overlay } from "./overlay"; -import MapLegend from "./legends/map-legend"; +import clusterLegend from "./legends/map-legend"; import UnitsLegend from "./legends/units-legend"; import TimelineLegend from "./legends/timeline-legend"; import { @@ -27,16 +27,6 @@ import { import MapSelectors from "./controls/map-selector"; import { cn } from "@/app/_lib/utils"; -import { - $Enums, - crime_categories, - crime_incidents, - crimes, - demographics, - districts, - geographics, - locations, -} from "@prisma/client"; import { CrimeTimelapse } from "./controls/bottom/crime-timelapse"; import { ITooltipsControl } from "./controls/top/tooltips"; import CrimeSidebar from "./controls/left/sidebar/map-sidebar"; @@ -54,6 +44,7 @@ import { } from "@/app/_utils/mock/ews-data"; import { useMap } from "react-map-gl/mapbox"; import PanicButtonDemo from "./controls/panic-button-demo"; +import ClusterLegend from "./legends/map-legend"; export default function CrimeMap() { const [sidebarCollapsed, setSidebarCollapsed] = useState(true); @@ -64,7 +55,7 @@ export default function CrimeMap() { IDistrictFeature | null >(null); const [selectedSourceType, setSelectedSourceType] = useState("cbu"); - const [selectedYear, setSelectedYear] = useState(2024); + const [selectedYear, setSelectedYear] = useState(); const [selectedMonth, setSelectedMonth] = useState("all"); const [selectedCategory, setSelectedCategory] = useState( "all", @@ -75,7 +66,6 @@ export default function CrimeMap() { const [useAllMonths, setUseAllMonths] = useState(false); const [showAllIncidents, setShowAllIncidents] = useState(false); - const [showLegend, setShowLegend] = useState(true); const [showUnitsLayer, setShowUnitsLayer] = useState(false); const [showClusters, setShowClusters] = useState(false); const [showHeatmap, setShowHeatmap] = useState(false); @@ -128,6 +118,12 @@ export default function CrimeMap() { const { data: recentIncidents } = useGetRecentIncidents(); + useEffect(() => { + const currentYear = new Date().getFullYear(); + const defaultYear = selectedSourceType === "cbu" ? 2024 : currentYear; + setSelectedYear(defaultYear); + }, [selectedSourceType]); + useEffect(() => { if ( activeControl === "heatmap" || activeControl === "timeline" || @@ -136,12 +132,13 @@ export default function CrimeMap() { setSelectedYear("all"); setUseAllYears(true); setUseAllMonths(true); - } else { - setSelectedYear(2024); + } else if (selectedYear === "all") { + const currentYear = new Date().getFullYear(); + setSelectedYear(selectedSourceType === "cbu" ? 2024 : currentYear); setUseAllYears(false); setUseAllMonths(false); } - }, [activeControl]); + }, [activeControl, selectedSourceType, selectedYear]); const crimesBySourceType = useMemo(() => { if (!crimes) return []; @@ -218,6 +215,20 @@ export default function CrimeMap() { setEwsIncidents(getAllIncidents()); }, []); + useEffect(() => { + const handleSetYear = (e: CustomEvent) => { + if (typeof e.detail === 'number') { + setSelectedYear(e.detail); + } + }; + + window.addEventListener('set-year', handleSetYear as EventListener); + + return () => { + window.removeEventListener('set-year', handleSetYear as EventListener); + }; + }, []); + const handleTriggerAlert = useCallback( (priority: "high" | "medium" | "low") => { const newIncident = addMockIncident({ priority }); @@ -243,6 +254,10 @@ export default function CrimeMap() { const handleSourceTypeChange = useCallback((sourceType: string) => { setSelectedSourceType(sourceType); + const currentYear = new Date().getFullYear(); + const defaultYear = sourceType === "cbu" ? 2024 : currentYear; + setSelectedYear(defaultYear); + if (sourceType === "cbu") { setActiveControl("clusters"); setShowClusters(true); @@ -270,16 +285,17 @@ export default function CrimeMap() { }, []); const resetFilters = useCallback(() => { - setSelectedYear(2024); + const currentYear = new Date().getFullYear(); + const defaultYear = selectedSourceType === "cbu" ? 2024 : currentYear; + setSelectedYear(defaultYear); setSelectedMonth("all"); setSelectedCategory("all"); - }, []); + }, [selectedSourceType]); const getMapTitle = () => { if (useAllYears) { - return `All Years Data ${ - selectedCategory !== "all" ? `- ${selectedCategory}` : "" - }`; + return `All Years Data ${selectedCategory !== "all" ? `- ${selectedCategory}` : "" + }`; } let title = `${selectedYear}`; @@ -357,13 +373,19 @@ export default function CrimeMap() { ); }, [crimes, crimesBySourceType, filteredCrimes, selectedSourceType]); + const disableYearMonth = activeControl === "incidents" || activeControl === "heatmap" || activeControl === "timeline" + + const activeIncidents = useMemo(() => { + return ewsIncidents.filter((incident) => incident.status === "active"); + }, [ewsIncidents]) + return ( Crime Map {getMapTitle()} ) : crimesError - ? ( -
- -

- Failed to load crime data. Please try again - later. -

- -
- ) - : ( -
+ ? ( +
+ +

+ Failed to load crime data. Please try again + later. +

+ +
+ ) + : (
-
- - - - {isFullscreen && ( - <> -
- -
- - {mapboxMap && ( - - )} - - {displayPanicDemo && ( -
- - inc.status === - "active" - )} - /> -
- )} - - - -
- {showClusters && ( - - )} - - {!showClusters && ( - - )} -
- - {showUnitsLayer && ( -
- -
- )} - - {showTimelineLayer && ( -
- -
- )} - - )} - -
- +
+ + -
- + + {isFullscreen && ( + <> +
+ +
+ + {mapboxMap && ( + + )} + + {displayPanicDemo && ( +
+ +
+ )} + + + +
+ {showClusters && ( + + )} +
+ + {showUnitsLayer && ( +
+ +
+ )} + + {showTimelineLayer && ( +
+ +
+ )} + + )} + +
+ +
+ +
-
- )} + )}
); diff --git a/sigap-website/app/_components/map/legends/map-legend.tsx b/sigap-website/app/_components/map/legends/map-legend.tsx index 80ce8dc..a721621 100644 --- a/sigap-website/app/_components/map/legends/map-legend.tsx +++ b/sigap-website/app/_components/map/legends/map-legend.tsx @@ -2,11 +2,11 @@ import { CRIME_RATE_COLORS } from "@/app/_utils/const/map"; import { Overlay } from "../overlay"; import { ControlPosition } from "mapbox-gl"; -interface MapLegendProps { +interface IClusterLegendProps { position?: ControlPosition } -export default function MapLegend({ position = "bottom-right" }: MapLegendProps) { +export default function ClusterLegend({ position = "bottom-right" }: IClusterLegendProps) { return ( //