feat: Enhance unit popup to display nearby incidents and loading state
This commit is contained in:
parent
2c11cc5991
commit
834d4b02cf
|
@ -18,6 +18,14 @@ interface UnitsLayerProps {
|
||||||
map?: mapboxgl.Map | null
|
map?: mapboxgl.Map | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IDistrictIncidents {
|
||||||
|
incident_id: string
|
||||||
|
category_name: string
|
||||||
|
incident_description: string
|
||||||
|
distance_meters: number
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
export default function UnitsLayer({ crimes, units = [], filterCategory, visible = false, map }: UnitsLayerProps) {
|
export default function UnitsLayer({ crimes, units = [], filterCategory, visible = false, map }: UnitsLayerProps) {
|
||||||
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
|
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
|
||||||
const loadedUnitsRef = useRef<IUnits[]>([])
|
const loadedUnitsRef = useRef<IUnits[]>([])
|
||||||
|
@ -28,6 +36,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
const [selectedEntityId, setSelectedEntityId] = useState<string | undefined>()
|
const [selectedEntityId, setSelectedEntityId] = useState<string | undefined>()
|
||||||
const [isUnitSelected, setIsUnitSelected] = useState<boolean>(false)
|
const [isUnitSelected, setIsUnitSelected] = useState<boolean>(false)
|
||||||
const [selectedDistrictId, setSelectedDistrictId] = useState<string | undefined>()
|
const [selectedDistrictId, setSelectedDistrictId] = useState<string | undefined>()
|
||||||
|
const [unitIncident, setUnitIncident] = useState<IDistrictIncidents[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
// Use either provided units or loaded units
|
// Use either provided units or loaded units
|
||||||
const unitsData = useMemo(() => {
|
const unitsData = useMemo(() => {
|
||||||
|
@ -230,6 +240,39 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Find all incidents in the same district as the unit
|
||||||
|
const districtIncidents: IDistrictIncidents[] = []
|
||||||
|
crimes.forEach(crime => {
|
||||||
|
// Check if this crime is in the same district as the unit
|
||||||
|
|
||||||
|
console.log("Checking district ID:", crime.district_id, "against unit district ID:", unit.district_id);
|
||||||
|
|
||||||
|
if (crime.districts.name === unit.district_name) {
|
||||||
|
crime.crime_incidents.forEach(incident => {
|
||||||
|
if (incident.locations && typeof incident.locations.distance_to_unit !== 'undefined') {
|
||||||
|
districtIncidents.push({
|
||||||
|
incident_id: incident.id,
|
||||||
|
category_name: incident.crime_categories.name,
|
||||||
|
incident_description: incident.description || 'No description',
|
||||||
|
distance_meters: incident.locations.distance_to_unit!,
|
||||||
|
timestamp: incident.timestamp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort by distance (closest first)
|
||||||
|
districtIncidents.sort((a, b) => a.distance_meters - b.distance_meters)
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Sorted district incidents:", districtIncidents);
|
||||||
|
|
||||||
|
// Update the state with the distance results
|
||||||
|
setUnitIncident(districtIncidents)
|
||||||
|
|
||||||
|
|
||||||
// Fly to the unit location
|
// Fly to the unit location
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
center: [unit.longitude || 0, unit.latitude || 0],
|
center: [unit.longitude || 0, unit.latitude || 0],
|
||||||
|
@ -266,7 +309,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
map.getCanvas().dispatchEvent(customEvent)
|
map.getCanvas().dispatchEvent(customEvent)
|
||||||
document.dispatchEvent(customEvent)
|
document.dispatchEvent(customEvent)
|
||||||
},
|
},
|
||||||
[],
|
[crimes], // Add crimes as a dependency
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle incident click
|
// Handle incident click
|
||||||
|
@ -441,6 +484,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
setSelectedIncident(null)
|
setSelectedIncident(null)
|
||||||
setSelectedEntityId(undefined)
|
setSelectedEntityId(undefined)
|
||||||
setSelectedDistrictId(undefined)
|
setSelectedDistrictId(undefined)
|
||||||
|
setUnitIncident([])
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
if (map && map.getLayer("units-connection-lines")) {
|
if (map && map.getLayer("units-connection-lines")) {
|
||||||
map.setFilter("units-connection-lines", ["has", "unit_id"])
|
map.setFilter("units-connection-lines", ["has", "unit_id"])
|
||||||
|
@ -540,6 +585,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
district: selectedUnit.district_name || "No district",
|
district: selectedUnit.district_name || "No district",
|
||||||
district_id: selectedUnit.district_id,
|
district_id: selectedUnit.district_id,
|
||||||
}}
|
}}
|
||||||
|
incidents={unitIncident}
|
||||||
|
isLoadingIncidents={isLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -23,17 +23,26 @@ interface UnitPopupProps {
|
||||||
district?: string
|
district?: string
|
||||||
district_id?: string
|
district_id?: string
|
||||||
}
|
}
|
||||||
distances?: IDistanceResult[]
|
incidents?: IDistrictIncidents[]
|
||||||
isLoadingDistances?: boolean
|
isLoadingIncidents?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IDistrictIncidents {
|
||||||
|
incident_id: string
|
||||||
|
category_name: string
|
||||||
|
incident_description: string
|
||||||
|
distance_meters: number
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function UnitPopup({
|
export default function UnitPopup({
|
||||||
longitude,
|
longitude,
|
||||||
latitude,
|
latitude,
|
||||||
onClose,
|
onClose,
|
||||||
unit,
|
unit,
|
||||||
distances = [],
|
incidents = [],
|
||||||
isLoadingDistances = false
|
isLoadingIncidents = false
|
||||||
}: UnitPopupProps) {
|
}: UnitPopupProps) {
|
||||||
|
|
||||||
// Format distance to be more readable
|
// Format distance to be more readable
|
||||||
|
@ -53,12 +62,12 @@ export default function UnitPopup({
|
||||||
closeOnClick={false}
|
closeOnClick={false}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
anchor="top"
|
anchor="top"
|
||||||
maxWidth="320px"
|
maxWidth="420px"
|
||||||
className="unit-popup z-50"
|
className="unit-popup z-50"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Card
|
<Card
|
||||||
className="bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 border-l-blue-700"
|
className="bg-background p-0 w-full max-w-[420px] shadow-xl border-0 overflow-hidden border-l-4 border-l-blue-700"
|
||||||
>
|
>
|
||||||
<div className="p-4 relative">
|
<div className="p-4 relative">
|
||||||
{/* Custom close button */}
|
{/* Custom close button */}
|
||||||
|
@ -114,7 +123,7 @@ export default function UnitPopup({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Distances to incidents section */}
|
{/* Incidents to incidents section */}
|
||||||
<Separator className="my-3" />
|
<Separator className="my-3" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -123,16 +132,16 @@ export default function UnitPopup({
|
||||||
Nearby Incidents
|
Nearby Incidents
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{isLoadingDistances ? (
|
{isLoadingIncidents ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-6 w-full" />
|
<Skeleton className="h-6 w-full" />
|
||||||
<Skeleton className="h-6 w-full" />
|
<Skeleton className="h-6 w-full" />
|
||||||
<Skeleton className="h-6 w-full" />
|
<Skeleton className="h-6 w-full" />
|
||||||
</div>
|
</div>
|
||||||
) : distances.length > 0 ? (
|
) : incidents.length > 0 ? (
|
||||||
<ScrollArea className="h-[120px] rounded-md border p-2">
|
<ScrollArea className="h-[120px] rounded-md border p-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{distances.map((item) => (
|
{incidents.map((item) => (
|
||||||
<div key={item.incident_id} className="flex justify-between items-center text-xs border-b pb-1">
|
<div key={item.incident_id} className="flex justify-between items-center text-xs border-b pb-1">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{item.category_name || "Unknown"}</p>
|
<p className="font-medium">{item.category_name || "Unknown"}</p>
|
||||||
|
@ -140,7 +149,7 @@ export default function UnitPopup({
|
||||||
{item.incident_description || "No description"}
|
{item.incident_description || "No description"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="ml-2 whitespace-nowrap">
|
<Badge variant="outline" className="ml-2 whitespace-nowrap text-blue-600">
|
||||||
{formatDistance(item.distance_meters)}
|
{formatDistance(item.distance_meters)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
@ -163,28 +172,7 @@ export default function UnitPopup({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{/* Connection line */}
|
|
||||||
<div
|
|
||||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
|
||||||
style={{
|
|
||||||
width: '2px',
|
|
||||||
height: '20px',
|
|
||||||
backgroundColor: 'red',
|
|
||||||
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* Connection dot */}
|
|
||||||
<div
|
|
||||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
|
||||||
style={{
|
|
||||||
width: '6px',
|
|
||||||
height: '6px',
|
|
||||||
backgroundColor: 'red',
|
|
||||||
borderRadius: '50%',
|
|
||||||
marginTop: '20px',
|
|
||||||
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue