1- import { useState , useEffect , useCallback , useRef , useMemo } from 'react' ;
1+ import { useState , useEffect , useCallback , useRef } from 'react' ;
22import { FaEye , FaXmark , FaMagnifyingGlass , FaChevronLeft , FaChevronRight } from 'react-icons/fa6' ;
33import axios from 'axios' ;
44import L from 'leaflet' ;
@@ -16,6 +16,7 @@ interface InfractionVessel {
1616 infractions_count : number ;
1717 highest_severity : 'low' | 'medium' | 'high' ;
1818 risk_score : number ;
19+ last_seen_at : string ;
1920}
2021
2122interface InfractionsPanelProps {
@@ -26,7 +27,6 @@ interface InfractionsPanelProps {
2627 onOpen ?: ( ) => void ;
2728 onClose ?: ( ) => void ;
2829 hideTrigger ?: boolean ;
29- trackedVessels ?: MapVessel [ ] ;
3030 isCompact ?: boolean ;
3131}
3232
@@ -38,7 +38,6 @@ export default function InfractionsPanel({
3838 onOpen,
3939 onClose,
4040 hideTrigger = false ,
41- trackedVessels = [ ] ,
4241 isCompact = false ,
4342} : InfractionsPanelProps ) {
4443 const [ isOpenInternal , setIsOpenInternal ] = useState ( false ) ;
@@ -47,14 +46,15 @@ export default function InfractionsPanel({
4746 const closePanel = onClose ?? ( ( ) => setIsOpenInternal ( false ) ) ;
4847
4948 const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
50- const [ severityFilter , setSeverityFilter ] = useState < 'all' | ' low' | 'medium' | 'high' > ( 'all ' ) ;
49+ const [ severityFilter , setSeverityFilter ] = useState < 'low' | 'medium' | 'high' > ( 'high ' ) ;
5150 const [ statusFilter , setStatusFilter ] = useState < 'all' | 'online' | 'offline' > ( 'all' ) ;
5251 const [ vessels , setVessels ] = useState < InfractionVessel [ ] > ( [ ] ) ;
5352 const [ loading , setLoading ] = useState ( false ) ;
5453 const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
5554 const [ totalPages , setTotalPages ] = useState ( 1 ) ;
5655 const [ totalVessels , setTotalVessels ] = useState ( 0 ) ;
5756 const [ checkingVesselMmsi , setCheckingVesselMmsi ] = useState < number | null > ( null ) ;
57+ const [ currentTime , setCurrentTime ] = useState ( ( ) => Date . now ( ) ) ;
5858 const ITEMS_PER_PAGE = 20 ;
5959
6060 const searchTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
@@ -67,6 +67,8 @@ export default function InfractionsPanel({
6767 L . DomEvent . disableScrollPropagation ( panelRef . current ) ;
6868 L . DomEvent . disableClickPropagation ( panelRef . current ) ;
6969 }
70+ const interval = setInterval ( ( ) => setCurrentTime ( Date . now ( ) ) , 60000 ) ;
71+ return ( ) => clearInterval ( interval ) ;
7072 } , [ isOpen ] ) ;
7173
7274 const fetchVessels = useCallback (
@@ -84,6 +86,10 @@ export default function InfractionsPanel({
8486 const controller = new AbortController ( ) ;
8587 abortControllerRef . current = controller ;
8688
89+ if ( page === 1 ) {
90+ setVessels ( [ ] ) ;
91+ }
92+
8793 try {
8894 const response = await axios . get ( `${ API_BASE_URL } /vessels/infractions` , {
8995 params : {
@@ -135,20 +141,9 @@ export default function InfractionsPanel({
135141 if ( abortControllerRef . current ) abortControllerRef . current . abort ( ) ;
136142 } ;
137143 } , [ searchQuery , severityFilter , statusFilter , isOpen , fetchVessels , currentPage ] ) ;
138-
139- const onlineMmsis = useMemo ( ( ) => {
140- const set = new Set < number > ( ) ;
141- trackedVessels . forEach ( ( v ) => {
142- const val = Number ( v . mmsi ) ;
143- if ( ! isNaN ( val ) ) set . add ( val ) ;
144- } ) ;
145- return set ;
146- } , [ trackedVessels ] ) ;
147-
148144 const handleVesselClick = async ( vessel : InfractionVessel ) => {
149145 setCheckingVesselMmsi ( vessel . mmsi ) ;
150146 try {
151- // Artificial delay to match the "intelligence verification" feel of the sanctions panel
152147 await new Promise ( ( resolve ) => setTimeout ( resolve , 600 ) ) ;
153148
154149 if ( onVesselSelect ) {
@@ -224,10 +219,11 @@ export default function InfractionsPanel({
224219
225220 < div className = "flex flex-col gap-2" >
226221 < div className = "flex bg-zinc-950 border border-white/20 divide-x divide-white/10 overflow-hidden w-full" >
227- { ( [ 'all' , ' low', 'medium' , 'high' ] as const ) . map ( ( sev ) => (
222+ { ( [ 'low' , 'medium' , 'high' ] as const ) . map ( ( sev ) => (
228223 < button
229224 key = { sev }
230225 onClick = { ( ) => {
226+ if ( severityFilter !== sev ) setVessels ( [ ] ) ;
231227 setSeverityFilter ( sev ) ;
232228 setCurrentPage ( 1 ) ;
233229 } }
@@ -246,6 +242,7 @@ export default function InfractionsPanel({
246242 < button
247243 key = { status }
248244 onClick = { ( ) => {
245+ if ( statusFilter !== status ) setVessels ( [ ] ) ;
249246 setStatusFilter ( status ) ;
250247 setCurrentPage ( 1 ) ;
251248 } }
@@ -278,7 +275,9 @@ export default function InfractionsPanel({
278275
279276 < div className = "space-y-1" >
280277 { vessels . map ( ( vessel ) => {
281- const isOnline = onlineMmsis . has ( vessel . mmsi ) ;
278+ const isOnline =
279+ new Date ( vessel . last_seen_at ) . getTime ( ) >=
280+ currentTime - 15 * 60 * 1000 ;
282281 return (
283282 < button
284283 key = { vessel . mmsi }
0 commit comments