feat(map): enhance CrimeSidebar with filtered crimes and incident click handling

This commit is contained in:
vergiLgood1 2025-05-04 03:59:56 +07:00
parent 7916680122
commit 593b198b94
2 changed files with 289 additions and 120 deletions

View File

@ -298,6 +298,7 @@ export default function CrimeMap() {
{/* Pass selectedCategory, selectedYear, and selectedMonth to the sidebar */} {/* Pass selectedCategory, selectedYear, and selectedMonth to the sidebar */}
<CrimeSidebar <CrimeSidebar
crimes={filteredCrimes || []}
defaultCollapsed={sidebarCollapsed} defaultCollapsed={sidebarCollapsed}
selectedCategory={selectedCategory} selectedCategory={selectedCategory}
selectedYear={selectedYear} selectedYear={selectedYear}

View File

@ -14,82 +14,37 @@ import { getMonthName, formatDateString } from "@/app/_utils/common"
import { Skeleton } from "@/app/_components/ui/skeleton" import { Skeleton } from "@/app/_components/ui/skeleton"
import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries" import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
import { $Enums } from "@prisma/client" import { $Enums } from "@prisma/client"
import { useMap } from "react-map-gl/mapbox"
import { ICrimes } from "@/app/_utils/types/crimes"
interface CrimeSidebarProps { interface CrimeSidebarProps {
className?: string className?: string
defaultCollapsed?: boolean defaultCollapsed?: boolean
selectedCategory?: string | "all" selectedCategory?: string | "all"
selectedYear?: number selectedYear: number
selectedMonth?: number | "all" selectedMonth?: number | "all"
} crimes: ICrimes[]
isLoading?: boolean
// Updated interface to match the structure returned by getCrimeByYearAndMonth
interface ICrimesProps {
id: string
district_id: string
districts: {
name: string
geographics?: {
address: string
land_area: number
year: number
}[]
demographics?: {
number_of_unemployed: number
population: number
population_density: number
year: number
}[]
}
number_of_crime: number
level: $Enums.crime_rates
score: number
month: number
year: number
crime_incidents: Array<{
id: string
timestamp: Date
description: string
status: string
crime_categories: {
name: string
type: string
}
locations: {
address: string
latitude: number
longitude: number
}
}>
} }
export default function CrimeSidebar({ export default function CrimeSidebar({
className, className,
defaultCollapsed = true, defaultCollapsed = true,
selectedCategory = "all", selectedCategory = "all",
selectedYear: propSelectedYear, selectedYear,
selectedMonth: propSelectedMonth selectedMonth,
crimes = [],
isLoading = false,
}: CrimeSidebarProps) { }: CrimeSidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed) const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed)
const [activeTab, setActiveTab] = useState("incidents") const [activeTab, setActiveTab] = useState("incidents")
const [activeIncidentTab, setActiveIncidentTab] = useState("recent")
const [currentTime, setCurrentTime] = useState<Date>(new Date()) const [currentTime, setCurrentTime] = useState<Date>(new Date())
const [location, setLocation] = useState<string>("Jember, East Java") const [location, setLocation] = useState<string>("Jember, East Java")
const [paginationState, setPaginationState] = useState<Record<string, number>>({})
const { const { current: map } = useMap()
availableYears,
isYearsLoading,
crimes,
isCrimesLoading,
crimesError,
selectedYear: hookSelectedYear,
selectedMonth: hookSelectedMonth,
} = usePrefetchedCrimeData()
// Use props for selectedYear and selectedMonth if provided, otherwise fall back to hook values
const selectedYear = propSelectedYear || hookSelectedYear
const selectedMonth = propSelectedMonth || hookSelectedMonth
// Update current time every minute for the real-time display
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { const timer = setInterval(() => {
setCurrentTime(new Date()) setCurrentTime(new Date())
@ -98,13 +53,11 @@ export default function CrimeSidebar({
return () => clearInterval(timer) return () => clearInterval(timer)
}, []) }, [])
// Format date with selected year and month if provided
const getDisplayDate = () => { const getDisplayDate = () => {
// If we have a specific month selected, use that for display
if (selectedMonth && selectedMonth !== 'all') { if (selectedMonth && selectedMonth !== 'all') {
const date = new Date() const date = new Date()
date.setFullYear(selectedYear) date.setFullYear(selectedYear)
date.setMonth(Number(selectedMonth) - 1) // Month is 0-indexed in JS Date date.setMonth(Number(selectedMonth) - 1)
return new Intl.DateTimeFormat('en-US', { return new Intl.DateTimeFormat('en-US', {
year: 'numeric', year: 'numeric',
@ -112,7 +65,6 @@ export default function CrimeSidebar({
}).format(date) }).format(date)
} }
// Otherwise show today's date
return new Intl.DateTimeFormat('en-US', { return new Intl.DateTimeFormat('en-US', {
weekday: 'long', weekday: 'long',
year: 'numeric', year: 'numeric',
@ -129,33 +81,23 @@ export default function CrimeSidebar({
hour12: true hour12: true
}).format(currentTime) }).format(currentTime)
const { data: categoriesData } = useGetCrimeCategories()
const crimeStats = useMemo(() => { const crimeStats = useMemo(() => {
// Return default values if crimes is undefined, null, or not an array
if (!crimes || !Array.isArray(crimes) || crimes.length === 0) return { if (!crimes || !Array.isArray(crimes) || crimes.length === 0) return {
todaysIncidents: 0, todaysIncidents: 0,
totalIncidents: 0, totalIncidents: 0,
recentIncidents: [], recentIncidents: [],
filteredIncidents: [],
categoryCounts: {}, categoryCounts: {},
districts: {}, districts: {},
incidentsByMonth: Array(12).fill(0), incidentsByMonth: Array(12).fill(0),
clearanceRate: 0 clearanceRate: 0,
incidentsByMonthDetail: {} as Record<string, any[]>,
availableMonths: [] as string[]
} }
// Make sure we have a valid array to work with
let filteredCrimes = [...crimes] let filteredCrimes = [...crimes]
if (selectedCategory !== "all") { const crimeIncidents = filteredCrimes.flatMap((crime: ICrimes) =>
filteredCrimes = crimes.filter((crime: ICrimesProps) =>
crime.crime_incidents.some(incident =>
incident.crime_categories.name === selectedCategory
)
)
}
// Collect all incidents from all crimes
const allIncidents = filteredCrimes.flatMap((crime: ICrimesProps) =>
crime.crime_incidents.map(incident => ({ crime.crime_incidents.map(incident => ({
id: incident.id, id: incident.id,
timestamp: incident.timestamp, timestamp: incident.timestamp,
@ -169,13 +111,13 @@ export default function CrimeSidebar({
})) }))
) )
const totalIncidents = allIncidents.length const totalIncidents = crimeIncidents.length
const today = new Date() const today = new Date()
const thirtyDaysAgo = new Date() const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(today.getDate() - 30) thirtyDaysAgo.setDate(today.getDate() - 30)
const recentIncidents = allIncidents const recentIncidents = crimeIncidents
.filter((incident) => { .filter((incident) => {
if (!incident?.timestamp) return false if (!incident?.timestamp) return false
const incidentDate = new Date(incident.timestamp) const incidentDate = new Date(incident.timestamp)
@ -187,6 +129,12 @@ export default function CrimeSidebar({
return bTime - aTime return bTime - aTime
}) })
const filteredIncidents = crimeIncidents.sort((a, b) => {
const bTime = b?.timestamp ? new Date(b.timestamp).getTime() : 0
const aTime = a?.timestamp ? new Date(a.timestamp).getTime() : 0
return bTime - aTime
})
const todaysIncidents = recentIncidents.filter((incident) => { const todaysIncidents = recentIncidents.filter((incident) => {
const incidentDate = incident?.timestamp const incidentDate = incident?.timestamp
? new Date(incident.timestamp) ? new Date(incident.timestamp)
@ -194,20 +142,20 @@ export default function CrimeSidebar({
return incidentDate.toDateString() === today.toDateString() return incidentDate.toDateString() === today.toDateString()
}).length }).length
const categoryCounts = allIncidents.reduce((acc: Record<string, number>, incident) => { const categoryCounts = crimeIncidents.reduce((acc: Record<string, number>, incident) => {
const category = incident?.category || 'Unknown' const category = incident?.category || 'Unknown'
acc[category] = (acc[category] || 0) + 1 acc[category] = (acc[category] || 0) + 1
return acc return acc
}, {} as Record<string, number>) }, {} as Record<string, number>)
const districts = filteredCrimes.reduce((acc: Record<string, number>, crime: ICrimesProps) => { const districts = filteredCrimes.reduce((acc: Record<string, number>, crime: ICrimes) => {
const districtName = crime.districts.name || 'Unknown' const districtName = crime.districts.name || 'Unknown'
acc[districtName] = (acc[districtName] || 0) + (crime.number_of_crime || 0) acc[districtName] = (acc[districtName] || 0) + (crime.number_of_crime || 0)
return acc return acc
}, {} as Record<string, number>) }, {} as Record<string, number>)
const incidentsByMonth = Array(12).fill(0) const incidentsByMonth = Array(12).fill(0)
allIncidents.forEach((incident) => { crimeIncidents.forEach((incident) => {
if (!incident?.timestamp) return; if (!incident?.timestamp) return;
const date = new Date(incident.timestamp) const date = new Date(incident.timestamp)
@ -217,25 +165,89 @@ export default function CrimeSidebar({
} }
}) })
const resolvedIncidents = allIncidents.filter(incident => const resolvedIncidents = crimeIncidents.filter(incident =>
incident?.status?.toLowerCase() === "resolved" incident?.status?.toLowerCase() === "resolved"
).length ).length
const clearanceRate = totalIncidents > 0 ? const clearanceRate = totalIncidents > 0 ?
Math.round((resolvedIncidents / totalIncidents) * 100) : 0 Math.round((resolvedIncidents / totalIncidents) * 100) : 0
const incidentsByMonthDetail: Record<string, any[]> = {}
const availableMonths: string[] = []
crimeIncidents.forEach(incident => {
if (!incident?.timestamp) return
const date = new Date(incident.timestamp)
const monthKey = `${date.getFullYear()}-${date.getMonth() + 1}`
if (!incidentsByMonthDetail[monthKey]) {
incidentsByMonthDetail[monthKey] = []
availableMonths.push(monthKey)
}
incidentsByMonthDetail[monthKey].push(incident)
})
Object.keys(incidentsByMonthDetail).forEach(monthKey => {
incidentsByMonthDetail[monthKey].sort((a, b) => {
const bTime = b?.timestamp ? new Date(b.timestamp).getTime() : 0
const aTime = a?.timestamp ? new Date(a.timestamp).getTime() : 0
return bTime - aTime
})
})
availableMonths.sort((a, b) => {
const [yearA, monthA] = a.split('-').map(Number)
const [yearB, monthB] = b.split('-').map(Number)
if (yearB !== yearA) return yearB - yearA
return monthB - monthA
})
return { return {
todaysIncidents, todaysIncidents,
totalIncidents, totalIncidents,
recentIncidents: recentIncidents.slice(0, 10), recentIncidents: recentIncidents.slice(0, 10),
filteredIncidents,
categoryCounts, categoryCounts,
districts, districts,
incidentsByMonth, incidentsByMonth,
clearanceRate clearanceRate,
incidentsByMonthDetail,
availableMonths
} }
}, [crimes, selectedCategory]) }, [crimes])
useEffect(() => {
if (crimeStats.availableMonths && crimeStats.availableMonths.length > 0) {
const initialState: Record<string, number> = {}
crimeStats.availableMonths.forEach(month => {
initialState[month] = 0
})
setPaginationState(initialState)
}
}, [crimeStats.availableMonths])
const formatMonthKey = (monthKey: string): string => {
const [year, month] = monthKey.split('-').map(Number)
return `${getMonthName(month)} ${year}`
}
const handlePageChange = (monthKey: string, direction: 'next' | 'prev') => {
setPaginationState(prev => {
const currentPage = prev[monthKey] || 0
const totalPages = Math.ceil((crimeStats.incidentsByMonthDetail[monthKey]?.length || 0) / 5)
if (direction === 'next' && currentPage < totalPages - 1) {
return { ...prev, [monthKey]: currentPage + 1 }
} else if (direction === 'prev' && currentPage > 0) {
return { ...prev, [monthKey]: currentPage - 1 }
}
return prev
})
}
// Generate a time period display for the current view
const getTimePeriodDisplay = () => { const getTimePeriodDisplay = () => {
if (selectedMonth && selectedMonth !== 'all') { if (selectedMonth && selectedMonth !== 'all') {
return `${getMonthName(Number(selectedMonth))} ${selectedYear}` return `${getMonthName(Number(selectedMonth))} ${selectedYear}`
@ -292,6 +304,30 @@ export default function CrimeSidebar({
return "Low" return "Low"
} }
const handleIncidentClick = (incident: any) => {
if (!map || !incident.longitude || !incident.latitude) return
map.flyTo({
center: [incident.longitude, incident.latitude],
zoom: 15,
pitch: 60,
bearing: 0,
duration: 1500,
easing: (t) => t * (2 - t),
})
const customEvent = new CustomEvent("incident_click", {
detail: incident,
bubbles: true
})
if (map.getMap().getCanvas()) {
map.getMap().getCanvas().dispatchEvent(customEvent)
} else {
document.dispatchEvent(customEvent)
}
}
return ( return (
<div className={cn( <div className={cn(
"fixed top-0 left-0 h-full z-40 transition-transform duration-300 ease-in-out bg-background border-r border-sidebar-border", "fixed top-0 left-0 h-full z-40 transition-transform duration-300 ease-in-out bg-background border-r border-sidebar-border",
@ -301,7 +337,6 @@ export default function CrimeSidebar({
<div className="relative h-full flex items-stretch"> <div className="relative h-full flex items-stretch">
<div className="bg-background backdrop-blur-sm border-r border-sidebar-border h-full w-[420px]"> <div className="bg-background backdrop-blur-sm border-r border-sidebar-border h-full w-[420px]">
<div className="p-4 text-sidebar-foreground h-full flex flex-col max-h-full overflow-hidden"> <div className="p-4 text-sidebar-foreground h-full flex flex-col max-h-full overflow-hidden">
{/* Header with improved styling */}
<CardHeader className="p-0 pb-4 shrink-0 relative"> <CardHeader className="p-0 pb-4 shrink-0 relative">
<div className="absolute top-0 right-0"> <div className="absolute top-0 right-0">
<Button <Button
@ -322,7 +357,7 @@ export default function CrimeSidebar({
<CardTitle className="text-xl font-semibold"> <CardTitle className="text-xl font-semibold">
Crime Analysis Crime Analysis
</CardTitle> </CardTitle>
{!isCrimesLoading && ( {!isLoading && (
<CardDescription className="text-sm text-sidebar-foreground/70"> <CardDescription className="text-sm text-sidebar-foreground/70">
{getTimePeriodDisplay()} {getTimePeriodDisplay()}
</CardDescription> </CardDescription>
@ -331,7 +366,6 @@ export default function CrimeSidebar({
</div> </div>
</CardHeader> </CardHeader>
{/* Improved tabs with pill style */}
<Tabs <Tabs
defaultValue="incidents" defaultValue="incidents"
className="w-full flex-1 flex flex-col overflow-hidden" className="w-full flex-1 flex flex-col overflow-hidden"
@ -361,7 +395,7 @@ export default function CrimeSidebar({
<div className="flex-1 overflow-y-auto overflow-x-hidden pr-1 custom-scrollbar"> <div className="flex-1 overflow-y-auto overflow-x-hidden pr-1 custom-scrollbar">
<TabsContent value="incidents" className="m-0 p-0 space-y-4"> <TabsContent value="incidents" className="m-0 p-0 space-y-4">
{isCrimesLoading ? ( {isLoading ? (
<div className="space-y-4"> <div className="space-y-4">
<Skeleton className="h-24 w-full" /> <Skeleton className="h-24 w-full" />
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@ -377,8 +411,7 @@ export default function CrimeSidebar({
</div> </div>
</div> </div>
) : ( ) : (
<> <>
{/* Enhanced info card */}
<Card className="bg-gradient-to-r from-sidebar-primary/30 to-sidebar-primary/20 border border-sidebar-primary/20 overflow-hidden"> <Card className="bg-gradient-to-r from-sidebar-primary/30 to-sidebar-primary/20 border border-sidebar-primary/20 overflow-hidden">
<CardContent className="p-4 text-sm relative"> <CardContent className="p-4 text-sm relative">
<div className="absolute top-0 right-0 w-24 h-24 bg-sidebar-primary/10 rounded-full -translate-y-1/2 translate-x-1/2"></div> <div className="absolute top-0 right-0 w-24 h-24 bg-sidebar-primary/10 rounded-full -translate-y-1/2 translate-x-1/2"></div>
@ -408,7 +441,6 @@ export default function CrimeSidebar({
</CardContent> </CardContent>
</Card> </Card>
{/* Enhanced stat cards */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<SystemStatusCard <SystemStatusCard
title="Total Cases" title="Total Cases"
@ -447,27 +479,49 @@ export default function CrimeSidebar({
/> />
</div> </div>
<SidebarSection <Tabs
title={selectedCategory !== "all" defaultValue="recent"
? `${selectedCategory} Incidents` value={activeIncidentTab}
: "Recent Incidents"} onValueChange={setActiveIncidentTab}
icon={<AlertTriangle className="h-4 w-4 text-red-400" />} className="w-full"
> >
{crimeStats.recentIncidents.length === 0 ? ( <div className="flex items-center justify-between mb-3">
<Card className="bg-white/5 border-0 text-white shadow-none"> <h3 className="text-sm font-medium text-sidebar-foreground/90 flex items-center gap-2 pl-1">
<CardContent className="p-4 text-center"> <AlertTriangle className="h-4 w-4 text-red-400" />
<div className="flex flex-col items-center gap-2"> Incident Reports
<AlertCircle className="h-6 w-6 text-white/40" /> </h3>
<p className="text-sm text-white/70"> <TabsList className="bg-sidebar-accent p-0.5 rounded-md h-7">
{selectedCategory !== "all" <TabsTrigger
? `No ${selectedCategory} incidents found` value="recent"
: "No recent incidents reported"} className="text-xs px-3 py-0.5 h-6 rounded-sm data-[state=active]:bg-sidebar-primary data-[state=active]:text-sidebar-primary-foreground"
</p> >
<p className="text-xs text-white/50">Try adjusting your filters or checking back later</p> Recent
</div> </TabsTrigger>
</CardContent> <TabsTrigger
</Card> value="history"
) : ( className="text-xs px-3 py-0.5 h-6 rounded-sm data-[state=active]:bg-sidebar-primary data-[state=active]:text-sidebar-primary-foreground"
>
History
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="recent" className="m-0 p-0">
{crimeStats.recentIncidents.length === 0 ? (
<Card className="bg-white/5 border-0 text-white shadow-none">
<CardContent className="p-4 text-center">
<div className="flex flex-col items-center gap-2">
<AlertCircle className="h-6 w-6 text-white/40" />
<p className="text-sm text-white/70">
{selectedCategory !== "all"
? `No ${selectedCategory} incidents found`
: "No recent incidents reported"}
</p>
<p className="text-xs text-white/50">Try adjusting your filters or checking back later</p>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-3"> <div className="space-y-3">
{crimeStats.recentIncidents.slice(0, 6).map((incident) => ( {crimeStats.recentIncidents.slice(0, 6).map((incident) => (
<IncidentCard <IncidentCard
@ -476,17 +530,117 @@ export default function CrimeSidebar({
time={incident.timestamp ? getTimeAgo(incident.timestamp) : 'Unknown time'} time={incident.timestamp ? getTimeAgo(incident.timestamp) : 'Unknown time'}
location={incident.address?.split(',').slice(1, 3).join(', ') || 'Unknown Location'} location={incident.address?.split(',').slice(1, 3).join(', ') || 'Unknown Location'}
severity={getIncidentSeverity(incident)} severity={getIncidentSeverity(incident)}
onClick={() => handleIncidentClick(incident)}
/> />
))} ))}
</div> </div>
)} )}
</SidebarSection> </TabsContent>
<TabsContent value="history" className="m-0 p-0">
{crimeStats.availableMonths && crimeStats.availableMonths.length === 0 ? (
<Card className="bg-white/5 border-0 text-white shadow-none">
<CardContent className="p-4 text-center">
<div className="flex flex-col items-center gap-2">
<FileText className="h-6 w-6 text-white/40" />
<p className="text-sm text-white/70">
{selectedCategory !== "all"
? `No ${selectedCategory} incidents found in the selected period`
: "No incidents found in the selected period"}
</p>
<p className="text-xs text-white/50">Try adjusting your filters</p>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-6">
<div className="flex justify-between items-center mb-1">
<span className="text-xs text-white/60">
Showing incidents from {crimeStats.availableMonths.length} {crimeStats.availableMonths.length === 1 ? 'month' : 'months'}
</span>
<Badge variant="outline" className="h-5 text-[10px]">
{selectedCategory !== "all" ? selectedCategory : "All Categories"}
</Badge>
</div>
{crimeStats.availableMonths.map(monthKey => {
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 (
<div key={monthKey} className="mb-5">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5">
<Calendar className="h-3.5 w-3.5 text-amber-400" />
<h4 className="font-medium text-xs">{formatMonthKey(monthKey)}</h4>
</div>
<Badge variant="secondary" className="h-5 text-[10px]">
{incidents.length} incident{incidents.length !== 1 ? 's' : ''}
</Badge>
</div>
<div className="space-y-2">
{paginatedIncidents.map((incident) => (
<IncidentCard
key={incident.id}
title={`${incident.category || 'Unknown'} in ${incident.address?.split(',')[0] || 'Unknown Location'}`}
time={incident.timestamp ? formatDateString(incident.timestamp) : 'Unknown date'}
location={incident.address?.split(',').slice(1, 3).join(', ') || 'Unknown Location'}
severity={getIncidentSeverity(incident)}
onClick={() => handleIncidentClick(incident)}
showTimeAgo={false}
/>
))}
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between mt-2">
<span className="text-xs text-white/50">
Page {currentPage + 1} of {totalPages}
</span>
<div className="flex gap-1">
<Button
variant="outline"
size="sm"
className="h-7 px-2 py-1 text-[10px]"
disabled={currentPage === 0}
onClick={() => handlePageChange(monthKey, 'prev')}
>
<ChevronLeft className="h-3 w-3 mr-1" />
Prev
</Button>
<Button
variant="outline"
size="sm"
className="h-7 px-2 py-1 text-[10px]"
disabled={currentPage >= totalPages - 1}
onClick={() => handlePageChange(monthKey, 'next')}
>
Next
<ChevronRight className="h-3 w-3 ml-1" />
</Button>
</div>
</div>
)}
</div>
)
})}
</div>
)}
</TabsContent>
</Tabs>
</> </>
)} )}
</TabsContent> </TabsContent>
<TabsContent value="statistics" className="m-0 p-0 space-y-4"> <TabsContent value="statistics" className="m-0 p-0 space-y-4">
{isCrimesLoading ? ( {isLoading ? (
<div className="space-y-4"> <div className="space-y-4">
<Skeleton className="h-64 w-full" /> <Skeleton className="h-64 w-full" />
<Skeleton className="h-24 w-full" /> <Skeleton className="h-24 w-full" />
@ -795,9 +949,11 @@ interface EnhancedIncidentCardProps {
time: string time: string
location: string location: string
severity: "Low" | "Medium" | "High" | "Critical" severity: "Low" | "Medium" | "High" | "Critical"
onClick?: () => void
showTimeAgo?: boolean
} }
function IncidentCard({ title, time, location, severity }: EnhancedIncidentCardProps) { function IncidentCard({ title, time, location, severity, onClick, showTimeAgo = true }: EnhancedIncidentCardProps) {
const getBadgeColor = () => { const getBadgeColor = () => {
switch (severity) { switch (severity) {
case "Low": return "bg-green-500/20 text-green-300"; case "Low": return "bg-green-500/20 text-green-300";
@ -819,7 +975,10 @@ function IncidentCard({ title, time, location, severity }: EnhancedIncidentCardP
}; };
return ( return (
<Card className={`bg-white/5 hover:bg-white/10 border-0 text-white shadow-none transition-colors border-l-2 ${getBorderColor()}`}> <Card
className={`bg-white/5 hover:bg-white/10 border-0 text-white shadow-none transition-colors border-l-2 ${getBorderColor()} ${onClick ? 'cursor-pointer' : ''}`}
onClick={onClick}
>
<CardContent className="p-3 text-xs"> <CardContent className="p-3 text-xs">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" /> <AlertTriangle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
@ -832,7 +991,16 @@ function IncidentCard({ title, time, location, severity }: EnhancedIncidentCardP
<MapPin className="h-3 w-3" /> <MapPin className="h-3 w-3" />
<span>{location}</span> <span>{location}</span>
</div> </div>
<div className="mt-1.5 text-white/60">{time}</div> <div className="mt-1.5 text-white/60 flex items-center gap-1">
{showTimeAgo ? (
time
) : (
<>
<Calendar className="h-3 w-3" />
<span>{time}</span>
</>
)}
</div>
</div> </div>
</div> </div>
</CardContent> </CardContent>