11import { Alert , CircularProgress , Grid , Typography } from '@mui/material'
2- import { useCallback , useContext , useEffect , useMemo , useState } from 'react'
2+ import { useQuery } from '@tanstack/react-query'
3+ import { useCallback , useContext , useMemo } from 'react'
34import { useTranslation } from 'react-i18next'
45import dayjs , { ISRAEL_TIMEZONE } from 'src/dayjs'
6+ import { usePageState } from 'src/hooks/usePageState'
57import { GlobalSearchContext } from 'src/model/globalState'
68import { INPUT_SIZE } from 'src/resources/sizes'
7- import { Gap , getGapsAsync } from '../../api/gapsService'
9+ import { getGapsAsync , SerializedGap , serializeGap } from '../../api/gapsService'
810import { getServiceDayRoutes } from '../../api/serviceDayRoutesService'
9- import { BusRoute } from '../../model/busRoute'
1011import { DateSelector } from '../components/DateSelector'
1112import { Label } from '../components/Label'
1213import LineNumberSelector from '../components/LineSelector'
@@ -22,9 +23,13 @@ const GapsPage = () => {
2223 const { t } = useTranslation ( )
2324 const { search, setSearch } = useContext ( GlobalSearchContext )
2425 const { operatorId, lineNumber, date, routeKey } = search
25- const [ routes , setRoutes ] = useState < BusRoute [ ] | undefined > ( )
26- const [ gaps , setGaps ] = useState < Gap [ ] > ( )
27- const [ gapsIsLoading , setGapsIsLoading ] = useState ( false )
26+
27+ // scrollPosition (auto-restored by usePageState) and the "only gaps" toggle are
28+ // device-local UI state, kept out of the shareable params.
29+ const { ui, setUi } = usePageState ( 'gaps' , {
30+ params : { } ,
31+ ui : { scrollPosition : 0 , gapsOnly : false } ,
32+ } )
2833
2934 const singleLineMapBaseHref = useMemo ( ( ) => {
3035 const params = new URLSearchParams ( )
@@ -35,48 +40,39 @@ const GapsPage = () => {
3540 return `/single-line-map?${ params . toString ( ) } `
3641 } , [ search . date , search . lineNumber , search . operatorId , search . routeKey ] )
3742
38- useEffect ( ( ) => {
39- if ( ! ( operatorId && routes && routeKey && date ) ) return
40- const selectedRoute = routes . find ( ( route ) => route . key === routeKey )
41- if ( ! selectedRoute ) return
42-
43- setGapsIsLoading ( true )
44- const { start, end } = serviceDayBounds ( date )
45- getGapsAsync ( start , end , operatorId , selectedRoute . lineRef )
46- . then ( ( res ) =>
47- setGaps (
48- res . filter ( ( g ) => {
49- const t = g . plannedStartTime || g . actualStartTime
50- return t && ! t . isBefore ( start ) && t . isBefore ( end )
51- } ) ,
52- ) ,
53- )
54- . catch ( ( err ) => {
55- console . error ( 'Failed to fetch gaps:' , err . message )
56- setGaps ( undefined )
57- } )
58- . finally ( ( ) => setGapsIsLoading ( false ) )
59- } , [ operatorId , routes , routeKey , date ] )
60-
61- useEffect ( ( ) => {
62- if ( ! operatorId || ! lineNumber ) {
63- return
64- }
65-
66- const controller = new AbortController ( )
43+ const routesQuery = useQuery ( {
44+ queryFn : ( { signal } ) => {
45+ if ( ! operatorId || ! lineNumber ) return null
46+ return getServiceDayRoutes ( dayjs . tz ( date , ISRAEL_TIMEZONE ) , operatorId , lineNumber , signal )
47+ } ,
48+ queryKey : [ 'gapsRoutes' , operatorId , lineNumber , date ] ,
49+ } )
50+ const routes = routesQuery . data ?? undefined
6751
68- getServiceDayRoutes ( dayjs . tz ( date , ISRAEL_TIMEZONE ) , operatorId , lineNumber , controller . signal )
69- . then ( ( fetchedRoutes ) => {
70- if ( search . lineNumber === lineNumber ) {
71- setRoutes ( fetchedRoutes )
72- }
73- } )
74- . catch ( ( err ) => {
75- console . error ( 'Failed to fetch routes:' , err . message )
76- } )
52+ const selectedRoute = useMemo (
53+ ( ) => routes ?. find ( ( route ) => route . key === routeKey ) ,
54+ [ routes , routeKey ] ,
55+ )
7756
78- return ( ) => controller . abort ( )
79- } , [ operatorId , lineNumber , date , setSearch ] )
57+ const gapsQuery = useQuery ( {
58+ queryFn : async ( ) : Promise < SerializedGap [ ] | null > => {
59+ if ( ! operatorId || ! selectedRoute || ! date ) return null
60+ const { start, end } = serviceDayBounds ( date )
61+ const res = await getGapsAsync ( start , end , operatorId , selectedRoute . lineRef )
62+ return (
63+ res
64+ . filter ( ( g ) => {
65+ const gapTime = g . plannedStartTime || g . actualStartTime
66+ return gapTime && ! gapTime . isBefore ( start ) && gapTime . isBefore ( end )
67+ } )
68+ // Store JSON-serializable strings, not dayjs, so the persisted cache
69+ // rehydrates losslessly; GapsTable revives them to dayjs on read.
70+ . map ( serializeGap )
71+ )
72+ } ,
73+ queryKey : [ 'gaps' , operatorId , selectedRoute ?. lineRef , date ] ,
74+ } )
75+ const gaps = gapsQuery . data ?? undefined
8076
8177 const handleDateChange = ( time : dayjs . Dayjs | null ) => {
8278 if ( ! time ) return
@@ -87,7 +83,9 @@ const GapsPage = () => {
8783 }
8884
8985 const handleOperatorChange = ( operatorId : string ) => {
90- setSearch ( ( current ) => ( { ...current , operatorId } ) )
86+ // Changing/clearing the operator invalidates the chosen route (routes are
87+ // per operator+line), so reset it to close the stale results table.
88+ setSearch ( ( current ) => ( { ...current , operatorId, routeKey : null } ) )
9189 }
9290
9391 const handleLineNumberChange = ( lineNumber : string ) => {
@@ -96,9 +94,6 @@ const GapsPage = () => {
9694 ? { ...current }
9795 : { ...current , lineNumber, routeKey : null } ,
9896 )
99- if ( lineNumber !== search . lineNumber ) {
100- setRoutes ( undefined )
101- }
10297 }
10398
10499 const handleRouteKeyChange = ( routeKey ?: string ) => {
@@ -123,36 +118,28 @@ const GapsPage = () => {
123118 < Alert severity = "info" variant = "outlined" icon = { false } >
124119 { t ( 'gaps_page_description' ) }
125120 </ Alert >
126- < Grid container spacing = { 2 } sx = { { maxWidth : INPUT_SIZE } } >
121+ < Grid container spacing = { 2 } sx = { { maxWidth : INPUT_SIZE , width : '100%' , mx : 'auto' } } >
127122 { /* choose date */ }
128- < Grid size = { { xs : 4 } } >
129- < Label text = { t ( 'choose_date' ) } />
130- </ Grid >
131- < Grid size = { { xs : 8 } } >
123+ < Grid size = { { sm : 6 , xs : 12 } } >
132124 < DateSelector time = { dayjs . tz ( date , ISRAEL_TIMEZONE ) } onChange = { handleDateChange } />
133125 </ Grid >
134126 { /* choose operator */ }
135- < Grid size = { { xs : 4 } } >
136- < Label text = { t ( 'choose_operator' ) } />
137- </ Grid >
138- < Grid size = { { xs : 8 } } >
127+ < Grid size = { { sm : 6 , xs : 12 } } >
139128 < OperatorSelector
140129 operatorId = { operatorId ?? undefined }
141130 setOperatorId = { handleOperatorChange }
142131 />
143132 </ Grid >
144133 { /* choose line */ }
145- < Grid size = { { xs : 4 } } >
146- < Label text = { t ( 'choose_line' ) } />
147- </ Grid >
148- < Grid size = { { xs : 8 } } >
134+ < Grid size = { { sm : 6 , xs : 12 } } >
149135 < LineNumberSelector
136+ disabled = { ! operatorId }
150137 lineNumber = { lineNumber ?? undefined }
151138 setLineNumber = { handleLineNumberChange }
152139 />
153140 </ Grid >
154- { /* choose routes */ }
155- < Grid size = { { xs : 12 } } >
141+ { /* choose route */ }
142+ < Grid size = { { sm : 6 , xs : 12 } } >
156143 { routes ?. length === 0 ? (
157144 < NotFound > { t ( 'line_not_found' ) } </ NotFound >
158145 ) : (
@@ -164,22 +151,24 @@ const GapsPage = () => {
164151 />
165152 ) }
166153 </ Grid >
167- < Grid size = { { xs : 12 } } >
168- { gapsIsLoading && (
154+ { gapsQuery . isLoading && (
155+ < Grid size = { { xs : 12 } } >
169156 < Row >
170157 < Label text = { t ( 'loading_gaps' ) } />
171158 < CircularProgress />
172159 </ Row >
173- ) }
174- </ Grid >
160+ </ Grid >
161+ ) }
175162 </ Grid >
176- { routeKey && routeKey !== '' && (
163+ { selectedRoute && (
177164 < GapsTable
178- loading = { gapsIsLoading }
165+ loading = { gapsQuery . isLoading }
179166 gaps = { gaps }
180167 date = { date }
181168 singleLineMapBaseHref = { singleLineMapBaseHref }
182169 onStartTimeClick = { handleStartTimeClick }
170+ onlyGapped = { ui . gapsOnly }
171+ onOnlyGappedChange = { ( value ) => setUi ( ( prev ) => ( { ...prev , gapsOnly : value } ) ) }
183172 />
184173 ) }
185174 </ PageContainer >
0 commit comments