refactor: update crime-related interfaces for consistency and clarity across components

This commit is contained in:
vergiLgood1 2025-05-05 03:11:07 +07:00
parent 2aa2e609f1
commit a2f51e6837
3 changed files with 117 additions and 137 deletions

View File

@ -13,27 +13,29 @@ import { ITooltips } from "./tooltips"
import { $Enums } from "@prisma/client" import { $Enums } from "@prisma/client"
// Define types based on the crime data structure // Define types based on the crime data structure
interface CrimeIncident { interface ICrimeIncident {
id: string id: string
timestamp: Date timestamp: Date
description: string description: string
status: string status: string
latitude?: number locations: {
longitude?: number address: string;
address?: string longitude: number;
crime_categories?: { latitude: number;
},
crime_categories: {
id: string id: string
name: string name: string
} }
} }
interface Crime { interface ICrime {
id: string id: string
district_id: string district_id: string
month: number month: number
year: number year: number
crime_incidents: CrimeIncident[] crime_incidents: ICrimeIncident[]
district: { districts: {
name: string name: string
} }
} }
@ -71,21 +73,21 @@ const ACTIONS = [
placeholder: "Enter crime description", placeholder: "Enter crime description",
}, },
{ {
id: "address", id: "locations.address",
label: "Search by Address", label: "Search by locations.Address",
icon: <FolderOpen className="h-4 w-4 text-amber-500" />, icon: <FolderOpen className="h-4 w-4 text-amber-500" />,
description: "e.g., Jalan Sudirman", description: "e.g., Jalan Sudirman",
category: "Search", category: "Search",
prefix: "", prefix: "",
regex: /.+/, regex: /.+/,
placeholder: "Enter location or address", placeholder: "Enter location or locations.address",
}, },
] ]
interface SearchTooltipProps { interface SearchTooltipProps {
onControlChange?: (controlId: ITooltips) => void onControlChange?: (controlId: ITooltips) => void
activeControl?: string activeControl?: string
crimes?: Crime[] crimes?: ICrime[]
} }
export default function SearchTooltip({ onControlChange, activeControl, crimes = [] }: SearchTooltipProps) { export default function SearchTooltip({ onControlChange, activeControl, crimes = [] }: SearchTooltipProps) {
@ -93,9 +95,9 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
const searchInputRef = useRef<HTMLInputElement>(null) const searchInputRef = useRef<HTMLInputElement>(null)
const [selectedSearchType, setSelectedSearchType] = useState<string | null>(null) const [selectedSearchType, setSelectedSearchType] = useState<string | null>(null)
const [searchValue, setSearchValue] = useState("") const [searchValue, setSearchValue] = useState("")
const [suggestions, setSuggestions] = useState<CrimeIncident[]>([]) const [suggestions, setSuggestions] = useState<ICrimeIncident[]>([])
const [isInputValid, setIsInputValid] = useState(true) const [isInputValid, setIsInputValid] = useState(true)
const [selectedSuggestion, setSelectedSuggestion] = useState<CrimeIncident | null>(null) const [selectedSuggestion, setSelectedSuggestion] = useState<ICrimeIncident | null>(null)
const [showInfoBox, setShowInfoBox] = useState(false) const [showInfoBox, setShowInfoBox] = useState(false)
// Limit results to prevent performance issues // Limit results to prevent performance issues
@ -105,7 +107,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
const allIncidents = crimes.flatMap(crime => const allIncidents = crimes.flatMap(crime =>
crime.crime_incidents.map(incident => ({ crime.crime_incidents.map(incident => ({
...incident, ...incident,
district: crime.district?.name || '', district: crime.districts?.name || '',
year: crime.year, year: crime.year,
month: crime.month month: crime.month
})) }))
@ -129,11 +131,11 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
setIsInputValid(true); setIsInputValid(true);
// Initial suggestions based on the selected search type // Initial suggestions based on the selected search type
let initialSuggestions: CrimeIncident[] = []; let initialSuggestions: ICrimeIncident[] = [];
if (actionId === 'incident_id') { if (actionId === 'incident_id') {
initialSuggestions = allIncidents.slice(0, MAX_RESULTS); // Limit to 50 results initially 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); 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 // Filter suggestions based on search type and search text
const filterSuggestions = (searchType: string, searchText: string): CrimeIncident[] => { const filterSuggestions = (searchType: string, searchText: string): ICrimeIncident[] => {
let filtered: CrimeIncident[] = []; let filtered: ICrimeIncident[] = [];
if (searchType === 'incident_id') { if (searchType === 'incident_id') {
if (!searchText || searchText === 'CI-') { if (!searchText || searchText === 'CI-') {
@ -175,26 +177,26 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
).slice(0, MAX_RESULTS); ).slice(0, MAX_RESULTS);
} }
} }
else if (searchType === 'address') { else if (searchType === 'locations.address') {
if (!searchText) { if (!searchText) {
filtered = allIncidents.slice(0, MAX_RESULTS); filtered = allIncidents.slice(0, MAX_RESULTS);
} else { } else {
filtered = allIncidents.filter(item => 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); ).slice(0, MAX_RESULTS);
} }
} }
else if (searchType === 'coordinates') { else if (searchType === 'coordinates') {
if (!searchText) { 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); .slice(0, MAX_RESULTS);
} else { } else {
// For coordinates, we'd typically do a proximity search // For coordinates, we'd typically do a proximity search
// This is a simple implementation for demo purposes // This is a simple implementation for demo purposes
filtered = allIncidents.filter(item => filtered = allIncidents.filter(item =>
item.latitude !== undefined && item.locations.latitude !== undefined &&
item.longitude !== undefined && item.locations.longitude !== undefined &&
`${item.latitude}, ${item.longitude}`.includes(searchText) `${item.locations.latitude}, ${item.locations.longitude}`.includes(searchText)
).slice(0, MAX_RESULTS); ).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); setSearchValue(incident.id);
setSuggestions([]); setSuggestions([]);
setSelectedSuggestion(incident); setSelectedSuggestion(incident);
@ -252,12 +254,12 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
}; };
const handleFlyToIncident = () => { 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: { detail: {
longitude: selectedSuggestion.longitude, longitude: selectedSuggestion.locations.longitude,
latitude: selectedSuggestion.latitude, latitude: selectedSuggestion.locations.latitude,
id: selectedSuggestion.id, id: selectedSuggestion.id,
zoom: 15, zoom: 15,
description: selectedSuggestion.description, description: selectedSuggestion.description,
@ -294,7 +296,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
} }
// Format date for display // Format date for display
const formatIncidentDate = (incident: CrimeIncident) => { const formatIncidentDate = (incident: ICrimeIncident) => {
try { try {
if (incident.timestamp) { if (incident.timestamp) {
return format(new Date(incident.timestamp), 'PPP p'); return format(new Date(incident.timestamp), 'PPP p');
@ -410,11 +412,11 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
</span> </span>
) : selectedSearchType === 'coordinates' ? ( ) : selectedSearchType === 'coordinates' ? (
<span className="text-muted-foreground text-sm truncate max-w-[300px]"> <span className="text-muted-foreground text-sm truncate max-w-[300px]">
{incident.latitude}, {incident.longitude} - {incident.description} {incident.locations.latitude}, {incident.locations.longitude} - {incident.description}
</span> </span>
) : selectedSearchType === 'address' ? ( ) : selectedSearchType === 'locations.address' ? (
<span className="text-muted-foreground text-sm truncate max-w-[300px]"> <span className="text-muted-foreground text-sm truncate max-w-[300px]">
{incident.address || 'N/A'} {incident.locations.address || 'N/A'}
</span> </span>
) : ( ) : (
<span className="text-muted-foreground text-sm truncate max-w-[300px]"> <span className="text-muted-foreground text-sm truncate max-w-[300px]">
@ -485,10 +487,10 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
</div> </div>
)} )}
{selectedSuggestion.address && ( {selectedSuggestion.locations.address && (
<div className="grid grid-cols-[20px_1fr] gap-2 items-start"> <div className="grid grid-cols-[20px_1fr] gap-2 items-start">
<MapPin className="h-4 w-4 mt-1 text-muted-foreground" /> <MapPin className="h-4 w-4 mt-1 text-muted-foreground" />
<p className="text-sm">{selectedSuggestion.address}</p> <p className="text-sm">{selectedSuggestion.locations.address}</p>
</div> </div>
)} )}
@ -515,7 +517,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes =
variant="default" variant="default"
size="sm" size="sm"
onClick={handleFlyToIncident} onClick={handleFlyToIncident}
disabled={!selectedSuggestion.latitude || !selectedSuggestion.longitude} disabled={!selectedSuggestion.locations.latitude || !selectedSuggestion.locations.longitude}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<span>Fly to Incident</span> <span>Fly to Incident</span>

View File

@ -23,14 +23,15 @@ import CrimeSidebar from "./controls/left/sidebar/map-sidebar"
import Tooltips from "./controls/top/tooltips" import Tooltips from "./controls/top/tooltips"
// Updated CrimeIncident type to match the structure in crime_incidents // Updated CrimeIncident type to match the structure in crime_incidents
interface CrimeIncident { interface ICrimeIncident {
id: string id: string
timestamp: Date district?: string
description: string
status: string
category?: string category?: string
type?: string type_category?: string | null
address?: string description?: string
status: string
address?: string | null
timestamp?: Date
latitude?: number latitude?: number
longitude?: number longitude?: number
} }
@ -39,7 +40,7 @@ export default function CrimeMap() {
// State for sidebar // State for sidebar
const [sidebarCollapsed, setSidebarCollapsed] = useState(true) const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null) const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
const [selectedIncident, setSelectedIncident] = useState<CrimeIncident | null>(null) const [selectedIncident, setSelectedIncident] = useState<ICrimeIncident | null>(null)
const [showLegend, setShowLegend] = useState<boolean>(true) const [showLegend, setShowLegend] = useState<boolean>(true)
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all") const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
const [selectedYear, setSelectedYear] = useState<number>(2024) const [selectedYear, setSelectedYear] = useState<number>(2024)
@ -113,22 +114,55 @@ export default function CrimeMap() {
useEffect(() => { useEffect(() => {
const handleIncidentClickEvent = (e: CustomEvent) => { const handleIncidentClickEvent = (e: CustomEvent) => {
console.log("Received incident_click event:", e.detail); console.log("Received incident_click event:", e.detail);
if (e.detail) { if (!e.detail || !e.detail.id) {
if (!e.detail.longitude || !e.detail.latitude) { console.error("Invalid incident data in event:", e.detail);
console.error("Invalid incident coordinates in event:", e.detail); return;
return;
}
// When an incident is clicked, clear any selected district
setSelectedDistrict(null);
// Set the selected incident
setSelectedIncident(e.detail);
} }
}
// 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 // Add event listener to the map container and document
const mapContainer = mapContainerRef.current const mapContainer = mapContainerRef.current;
// Clean up previous listeners to prevent duplicates // Clean up previous listeners to prevent duplicates
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
@ -145,67 +179,11 @@ export default function CrimeMap() {
return () => { return () => {
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
if (mapContainer) { if (mapContainer) {
mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
} }
} };
}, []); }, [filteredCrimes]);
// 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);
}
}, []);
// Handle year-month timeline change // Handle year-month timeline change
const handleTimelineChange = useCallback((year: number, month: number, progress: number) => { const handleTimelineChange = useCallback((year: number, month: number, progress: number) => {
@ -310,7 +288,7 @@ export default function CrimeMap() {
longitude={selectedIncident.longitude} longitude={selectedIncident.longitude}
latitude={selectedIncident.latitude} latitude={selectedIncident.latitude}
onClose={() => setSelectedIncident(null)} onClose={() => setSelectedIncident(null)}
crime={selectedIncident} incident={selectedIncident}
/> />
</> </>

View File

@ -7,25 +7,25 @@ import { Separator } from "@/app/_components/ui/separator"
import { Button } from "@/app/_components/ui/button" import { Button } from "@/app/_components/ui/button"
import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText, Navigation, X } from "lucide-react" import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText, Navigation, X } from "lucide-react"
interface CrimePopupProps { interface IncidentPopupProps {
longitude: number longitude: number
latitude: number latitude: number
onClose: () => void onClose: () => void
crime: { incident: {
id: string id: string
district?: string district?: string
category?: string category?: string
type?: string type_category?: string | null
description?: string description?: string
status?: string status?: string
address?: string address?: string | null
timestamp?: Date timestamp?: Date
latitude?: number latitude?: number
longitude?: 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) => { const formatDate = (date?: Date) => {
if (!date) return "Unknown date" if (!date) return "Unknown date"
return new Date(date).toLocaleDateString() return new Date(date).toLocaleDateString()
@ -80,10 +80,10 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
onClose={onClose} onClose={onClose}
anchor="top" anchor="top"
maxWidth="320px" maxWidth="320px"
className="crime-popup z-50" className="incident-popup z-50"
> >
<Card <Card
className={`bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 ${getBorderColor(crime.status)}`} className={`bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 ${getBorderColor(incident.status)}`}
> >
<div className="p-4 relative"> <div className="p-4 relative">
{/* Custom close button */} {/* Custom close button */}
@ -100,16 +100,16 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-base flex items-center gap-1.5"> <h3 className="font-bold text-base flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-red-500" /> <AlertTriangle className="h-4 w-4 text-red-500" />
{crime.category || "Unknown Incident"} {incident.category || "Unknown Incident"}
</h3> </h3>
{getStatusBadge(crime.status)} {getStatusBadge(incident.status)}
</div> </div>
{crime.description && ( {incident.description && (
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg"> <div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
<p className="text-sm"> <p className="text-sm">
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" /> <FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
{crime.description} {incident.description}
</p> </p>
</div> </div>
)} )}
@ -118,51 +118,51 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
{/* Improved section headers */} {/* Improved section headers */}
<div className="grid grid-cols-2 gap-2 text-sm"> <div className="grid grid-cols-2 gap-2 text-sm">
{crime.district && ( {incident.district && (
<div className="col-span-2"> <div className="col-span-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
<p className="flex items-center"> <p className="flex items-center">
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" /> <Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
<span className="font-medium">{crime.district}</span> <span className="font-medium">{incident.district}</span>
</p> </p>
</div> </div>
)} )}
{crime.address && ( {incident.address && (
<div className="col-span-2"> <div className="col-span-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Location</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Location</p>
<p className="flex items-center"> <p className="flex items-center">
<MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" /> <MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" />
<span className="font-medium">{crime.address}</span> <span className="font-medium">{incident.address}</span>
</p> </p>
</div> </div>
)} )}
{crime.timestamp && ( {incident.timestamp && (
<> <>
<div> <div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
<p className="flex items-center"> <p className="flex items-center">
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" /> <Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
<span className="font-medium">{formatDate(crime.timestamp)}</span> <span className="font-medium">{formatDate(incident.timestamp)}</span>
</p> </p>
</div> </div>
<div> <div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
<p className="flex items-center"> <p className="flex items-center">
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" /> <Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
<span className="font-medium">{formatTime(crime.timestamp)}</span> <span className="font-medium">{formatTime(incident.timestamp)}</span>
</p> </p>
</div> </div>
</> </>
)} )}
{crime.type && ( {incident.type_category && (
<div className="col-span-2"> <div className="col-span-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p> <p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p>
<p className="flex items-center"> <p className="flex items-center">
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" /> <Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
<span className="font-medium">{crime.type}</span> <span className="font-medium">{incident.type_category}</span>
</p> </p>
</div> </div>
)} )}
@ -173,7 +173,7 @@ export default function CrimePopup({ longitude, latitude, onClose, crime }: Crim
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" /> <Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)} Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
</p> </p>
<p className="text-xs text-muted-foreground mt-1">ID: {crime.id}</p> <p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
</div> </div>
</div> </div>
</Card> </Card>