feat: Enhance unit popup to display nearby incidents and loading state

This commit is contained in:
vergiLgood1 2025-05-14 08:56:08 +07:00
parent 2c11cc5991
commit 834d4b02cf
2 changed files with 69 additions and 34 deletions

View File

@ -18,6 +18,14 @@ interface UnitsLayerProps {
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) {
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
const loadedUnitsRef = useRef<IUnits[]>([])
@ -28,6 +36,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
const [selectedEntityId, setSelectedEntityId] = useState<string | undefined>()
const [isUnitSelected, setIsUnitSelected] = useState<boolean>(false)
const [selectedDistrictId, setSelectedDistrictId] = useState<string | undefined>()
const [unitIncident, setUnitIncident] = useState<IDistrictIncidents[]>([])
const [isLoading, setIsLoading] = useState<boolean>(false)
// Use either provided units or loaded units
const unitsData = useMemo(() => {
@ -230,6 +240,39 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
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
map.flyTo({
center: [unit.longitude || 0, unit.latitude || 0],
@ -266,7 +309,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
map.getCanvas().dispatchEvent(customEvent)
document.dispatchEvent(customEvent)
},
[],
[crimes], // Add crimes as a dependency
)
// Handle incident click
@ -441,6 +484,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
setSelectedIncident(null)
setSelectedEntityId(undefined)
setSelectedDistrictId(undefined)
setUnitIncident([])
setIsLoading(false)
if (map && map.getLayer("units-connection-lines")) {
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_id: selectedUnit.district_id,
}}
incidents={unitIncident}
isLoadingIncidents={isLoading}
/>
)}

View File

@ -23,17 +23,26 @@ interface UnitPopupProps {
district?: string
district_id?: string
}
distances?: IDistanceResult[]
isLoadingDistances?: boolean
incidents?: IDistrictIncidents[]
isLoadingIncidents?: boolean
}
interface IDistrictIncidents {
incident_id: string
category_name: string
incident_description: string
distance_meters: number
timestamp: Date
}
export default function UnitPopup({
longitude,
latitude,
onClose,
unit,
distances = [],
isLoadingDistances = false
incidents = [],
isLoadingIncidents = false
}: UnitPopupProps) {
// Format distance to be more readable
@ -53,12 +62,12 @@ export default function UnitPopup({
closeOnClick={false}
onClose={onClose}
anchor="top"
maxWidth="320px"
maxWidth="420px"
className="unit-popup z-50"
>
<div className="relative">
<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">
{/* Custom close button */}
@ -114,7 +123,7 @@ export default function UnitPopup({
)}
</div>
{/* Distances to incidents section */}
{/* Incidents to incidents section */}
<Separator className="my-3" />
<div>
@ -123,16 +132,16 @@ export default function UnitPopup({
Nearby Incidents
</h4>
{isLoadingDistances ? (
{isLoadingIncidents ? (
<div className="space-y-2">
<Skeleton className="h-6 w-full" />
<Skeleton className="h-6 w-full" />
<Skeleton className="h-6 w-full" />
</div>
) : distances.length > 0 ? (
) : incidents.length > 0 ? (
<ScrollArea className="h-[120px] rounded-md border p-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>
<p className="font-medium">{item.category_name || "Unknown"}</p>
@ -140,7 +149,7 @@ export default function UnitPopup({
{item.incident_description || "No description"}
</p>
</div>
<Badge variant="outline" className="ml-2 whitespace-nowrap">
<Badge variant="outline" className="ml-2 whitespace-nowrap text-blue-600">
{formatDistance(item.distance_meters)}
</Badge>
</div>
@ -163,28 +172,7 @@ export default function UnitPopup({
</div>
</div>
</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>
</Popup>
)