diff --git a/sigap-website/app/_components/map/controls/top/search-control.tsx b/sigap-website/app/_components/map/controls/top/search-control.tsx index d31b5b9..f405c36 100644 --- a/sigap-website/app/_components/map/controls/top/search-control.tsx +++ b/sigap-website/app/_components/map/controls/top/search-control.tsx @@ -13,27 +13,29 @@ import { ITooltips } from "./tooltips" import { $Enums } from "@prisma/client" // Define types based on the crime data structure -interface CrimeIncident { +interface ICrimeIncident { id: string timestamp: Date description: string status: string - latitude?: number - longitude?: number - address?: string - crime_categories?: { + locations: { + address: string; + longitude: number; + latitude: number; + }, + crime_categories: { id: string name: string } } -interface Crime { +interface ICrime { id: string district_id: string month: number year: number - crime_incidents: CrimeIncident[] - district: { + crime_incidents: ICrimeIncident[] + districts: { name: string } } @@ -71,21 +73,21 @@ const ACTIONS = [ placeholder: "Enter crime description", }, { - id: "address", - label: "Search by Address", + id: "locations.address", + label: "Search by locations.Address", icon: , description: "e.g., Jalan Sudirman", category: "Search", prefix: "", regex: /.+/, - placeholder: "Enter location or address", + placeholder: "Enter location or locations.address", }, ] interface SearchTooltipProps { onControlChange?: (controlId: ITooltips) => void activeControl?: string - crimes?: Crime[] + crimes?: ICrime[] } export default function SearchTooltip({ onControlChange, activeControl, crimes = [] }: SearchTooltipProps) { @@ -93,9 +95,9 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = const searchInputRef = useRef(null) const [selectedSearchType, setSelectedSearchType] = useState(null) const [searchValue, setSearchValue] = useState("") - const [suggestions, setSuggestions] = useState([]) + const [suggestions, setSuggestions] = useState([]) const [isInputValid, setIsInputValid] = useState(true) - const [selectedSuggestion, setSelectedSuggestion] = useState(null) + const [selectedSuggestion, setSelectedSuggestion] = useState(null) const [showInfoBox, setShowInfoBox] = useState(false) // Limit results to prevent performance issues @@ -105,7 +107,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = const allIncidents = crimes.flatMap(crime => crime.crime_incidents.map(incident => ({ ...incident, - district: crime.district?.name || '', + district: crime.districts?.name || '', year: crime.year, month: crime.month })) @@ -129,11 +131,11 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = setIsInputValid(true); // Initial suggestions based on the selected search type - let initialSuggestions: CrimeIncident[] = []; + let initialSuggestions: ICrimeIncident[] = []; if (actionId === 'incident_id') { initialSuggestions = allIncidents.slice(0, MAX_RESULTS); // Limit to 50 results initially - } else if (actionId === 'description' || actionId === 'address') { + } else if (actionId === 'description' || actionId === 'locations.address') { initialSuggestions = allIncidents.slice(0, MAX_RESULTS); } @@ -154,8 +156,8 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = } // Filter suggestions based on search type and search text - const filterSuggestions = (searchType: string, searchText: string): CrimeIncident[] => { - let filtered: CrimeIncident[] = []; + const filterSuggestions = (searchType: string, searchText: string): ICrimeIncident[] => { + let filtered: ICrimeIncident[] = []; if (searchType === 'incident_id') { if (!searchText || searchText === 'CI-') { @@ -175,26 +177,26 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = ).slice(0, MAX_RESULTS); } } - else if (searchType === 'address') { + else if (searchType === 'locations.address') { if (!searchText) { filtered = allIncidents.slice(0, MAX_RESULTS); } else { filtered = allIncidents.filter(item => - item.address && item.address.toLowerCase().includes(searchText.toLowerCase()) + item.locations.address && item.locations.address.toLowerCase().includes(searchText.toLowerCase()) ).slice(0, MAX_RESULTS); } } else if (searchType === 'coordinates') { if (!searchText) { - filtered = allIncidents.filter(item => item.latitude !== undefined && item.longitude !== undefined) + filtered = allIncidents.filter(item => item.locations.latitude !== undefined && item.locations.longitude !== undefined) .slice(0, MAX_RESULTS); } else { // For coordinates, we'd typically do a proximity search // This is a simple implementation for demo purposes filtered = allIncidents.filter(item => - item.latitude !== undefined && - item.longitude !== undefined && - `${item.latitude}, ${item.longitude}`.includes(searchText) + item.locations.latitude !== undefined && + item.locations.longitude !== undefined && + `${item.locations.latitude}, ${item.locations.longitude}`.includes(searchText) ).slice(0, MAX_RESULTS); } } @@ -244,7 +246,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = } }; - const handleSuggestionSelect = (incident: CrimeIncident) => { + const handleSuggestionSelect = (incident: ICrimeIncident) => { setSearchValue(incident.id); setSuggestions([]); setSelectedSuggestion(incident); @@ -252,12 +254,12 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = }; const handleFlyToIncident = () => { - if (!selectedSuggestion || !selectedSuggestion.latitude || !selectedSuggestion.longitude) return; + if (!selectedSuggestion || !selectedSuggestion.locations.latitude || !selectedSuggestion.locations.longitude) return; - const flyToEvent = new CustomEvent('fly_to_incident', { + const flyToEvent = new CustomEvent('incident_click', { detail: { - longitude: selectedSuggestion.longitude, - latitude: selectedSuggestion.latitude, + longitude: selectedSuggestion.locations.longitude, + latitude: selectedSuggestion.locations.latitude, id: selectedSuggestion.id, zoom: 15, description: selectedSuggestion.description, @@ -294,7 +296,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = } // Format date for display - const formatIncidentDate = (incident: CrimeIncident) => { + const formatIncidentDate = (incident: ICrimeIncident) => { try { if (incident.timestamp) { return format(new Date(incident.timestamp), 'PPP p'); @@ -410,11 +412,11 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = ) : selectedSearchType === 'coordinates' ? ( - {incident.latitude}, {incident.longitude} - {incident.description} + {incident.locations.latitude}, {incident.locations.longitude} - {incident.description} - ) : selectedSearchType === 'address' ? ( + ) : selectedSearchType === 'locations.address' ? ( - {incident.address || 'N/A'} + {incident.locations.address || 'N/A'} ) : ( @@ -485,10 +487,10 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = )} - {selectedSuggestion.address && ( + {selectedSuggestion.locations.address && (
-

{selectedSuggestion.address}

+

{selectedSuggestion.locations.address}

)} @@ -515,7 +517,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = variant="default" size="sm" onClick={handleFlyToIncident} - disabled={!selectedSuggestion.latitude || !selectedSuggestion.longitude} + disabled={!selectedSuggestion.locations.latitude || !selectedSuggestion.locations.longitude} className="flex items-center gap-2" > Fly to Incident diff --git a/sigap-website/app/_components/map/crime-map.tsx b/sigap-website/app/_components/map/crime-map.tsx index 4fb2d5c..29a5388 100644 --- a/sigap-website/app/_components/map/crime-map.tsx +++ b/sigap-website/app/_components/map/crime-map.tsx @@ -23,14 +23,15 @@ import CrimeSidebar from "./controls/left/sidebar/map-sidebar" import Tooltips from "./controls/top/tooltips" // Updated CrimeIncident type to match the structure in crime_incidents -interface CrimeIncident { +interface ICrimeIncident { id: string - timestamp: Date - description: string - status: string + district?: string category?: string - type?: string - address?: string + type_category?: string | null + description?: string + status: string + address?: string | null + timestamp?: Date latitude?: number longitude?: number } @@ -39,7 +40,7 @@ export default function CrimeMap() { // State for sidebar const [sidebarCollapsed, setSidebarCollapsed] = useState(true) const [selectedDistrict, setSelectedDistrict] = useState(null) - const [selectedIncident, setSelectedIncident] = useState(null) + const [selectedIncident, setSelectedIncident] = useState(null) const [showLegend, setShowLegend] = useState(true) const [selectedCategory, setSelectedCategory] = useState("all") const [selectedYear, setSelectedYear] = useState(2024) @@ -113,22 +114,55 @@ export default function CrimeMap() { useEffect(() => { const handleIncidentClickEvent = (e: CustomEvent) => { console.log("Received incident_click event:", e.detail); - if (e.detail) { - if (!e.detail.longitude || !e.detail.latitude) { - console.error("Invalid incident coordinates in event:", e.detail); - return; - } - - // When an incident is clicked, clear any selected district - setSelectedDistrict(null); - - // Set the selected incident - setSelectedIncident(e.detail); + if (!e.detail || !e.detail.id) { + console.error("Invalid incident data in event:", e.detail); + return; } - } + + // Find the incident in filtered crimes data using the id from the event + let foundIncident: ICrimeIncident | undefined; + + // Search through all crimes and their incidents to find matching incident + filteredCrimes.forEach(crime => { + crime.crime_incidents.forEach(incident => { + if (incident.id === e.detail.id) { + // Map the found incident to ICrimeIncident type + foundIncident = { + id: incident.id, + district: crime.districts.name, + description: incident.description, + status: incident.status || "unknown", + timestamp: incident.timestamp, + category: incident.crime_categories.name, + type_category: incident.crime_categories.type, + address: incident.locations.address, + latitude: incident.locations.latitude, + longitude: incident.locations.longitude, + }; + } + }); + }); + + if (!foundIncident) { + console.error("Could not find incident with ID:", e.detail.id); + return; + } + + // Validate the coordinates + if (!foundIncident.latitude || !foundIncident.longitude) { + console.error("Invalid incident coordinates:", foundIncident); + return; + } + + // When an incident is clicked, clear any selected district + setSelectedDistrict(null); + + // Set the selected incident + setSelectedIncident(foundIncident); + }; // Add event listener to the map container and document - const mapContainer = mapContainerRef.current + const mapContainer = mapContainerRef.current; // Clean up previous listeners to prevent duplicates document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); @@ -145,67 +179,11 @@ export default function CrimeMap() { return () => { document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); - if (mapContainer) { mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); } - } - }, []); - - // Set up event listener for fly-to-incident events from search - useEffect(() => { - const handleFlyToIncident = (e: CustomEvent) => { - if (!e.detail || !e.detail.longitude || !e.detail.latitude) { - console.error("Invalid fly-to coordinates:", e.detail); - return; - } - - // Handle the fly-to event by dispatching to the map - const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map'); - if (mapInstance) { - // Clear any existing selections first - setSelectedIncident(null); - setSelectedDistrict(null); - - // Create an incident object to highlight - const incidentToHighlight: CrimeIncident = { - id: e.detail.id as string, - latitude: e.detail.latitude as number, - longitude: e.detail.longitude as number, - timestamp: new Date(), - description: e.detail.description || "", - status: e.detail.status - }; - - // First fly to the location - const flyEvent = new CustomEvent('mapbox_fly_to', { - detail: { - longitude: e.detail.longitude, - latitude: e.detail.latitude, - zoom: e.detail.zoom || 15, - bearing: 0, - pitch: 45, - duration: 2000, - }, - bubbles: true - }); - - mapInstance.dispatchEvent(flyEvent); - - // After flying, select the incident with a slight delay - setTimeout(() => { - setSelectedIncident(incidentToHighlight); - }, 2000); - } - } - - // Add event listener - document.addEventListener('fly_to_incident', handleFlyToIncident as EventListener); - - return () => { - document.removeEventListener('fly_to_incident', handleFlyToIncident as EventListener); - } - }, []); + }; + }, [filteredCrimes]); // Handle year-month timeline change const handleTimelineChange = useCallback((year: number, month: number, progress: number) => { @@ -310,7 +288,7 @@ export default function CrimeMap() { longitude={selectedIncident.longitude} latitude={selectedIncident.latitude} onClose={() => setSelectedIncident(null)} - crime={selectedIncident} + incident={selectedIncident} /> diff --git a/sigap-website/app/_components/map/pop-up/crime-popup.tsx b/sigap-website/app/_components/map/pop-up/crime-popup.tsx index c2adc8e..87dddc4 100644 --- a/sigap-website/app/_components/map/pop-up/crime-popup.tsx +++ b/sigap-website/app/_components/map/pop-up/crime-popup.tsx @@ -7,25 +7,25 @@ import { Separator } from "@/app/_components/ui/separator" import { Button } from "@/app/_components/ui/button" import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText, Navigation, X } from "lucide-react" -interface CrimePopupProps { +interface IncidentPopupProps { longitude: number latitude: number onClose: () => void - crime: { + incident: { id: string district?: string category?: string - type?: string + type_category?: string | null description?: string status?: string - address?: string + address?: string | null timestamp?: Date latitude?: number longitude?: number } } -export default function CrimePopup({ longitude, latitude, onClose, crime }: CrimePopupProps) { +export default function IncidentPopup({ longitude, latitude, onClose, incident }: IncidentPopupProps) { const formatDate = (date?: Date) => { if (!date) return "Unknown date" return new Date(date).toLocaleDateString() @@ -80,10 +80,10 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim onClose={onClose} anchor="top" maxWidth="320px" - className="crime-popup z-50" + className="incident-popup z-50" >
{/* Custom close button */} @@ -100,16 +100,16 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim

- {crime.category || "Unknown Incident"} + {incident.category || "Unknown Incident"}

- {getStatusBadge(crime.status)} + {getStatusBadge(incident.status)}
- {crime.description && ( + {incident.description && (

- {crime.description} + {incident.description}

)} @@ -118,51 +118,51 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim {/* Improved section headers */}
- {crime.district && ( + {incident.district && (

District

- {crime.district} + {incident.district}

)} - {crime.address && ( + {incident.address && (

Location

- {crime.address} + {incident.address}

)} - {crime.timestamp && ( + {incident.timestamp && ( <>

Date

- {formatDate(crime.timestamp)} + {formatDate(incident.timestamp)}

Time

- {formatTime(crime.timestamp)} + {formatTime(incident.timestamp)}

)} - {crime.type && ( + {incident.type_category && (

Type

- {crime.type} + {incident.type_category}

)} @@ -173,7 +173,7 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}

-

ID: {crime.id}

+

ID: {incident.id}