• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hasadna / open-bus-map-search / 14269590516

04 Apr 2025 04:12PM UTC coverage: 81.115% (-0.2%) from 81.34%
14269590516

push

github

web-flow
feat: see planned rout even when no actual ride was executed (#1070)

353 of 498 branches covered (70.88%)

Branch coverage included in aggregate %.

77 of 84 new or added lines in 6 files covered. (91.67%)

11 existing lines in 3 files now uncovered.

1000 of 1170 relevant lines covered (85.47%)

87327.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.24
/src/pages/gaps/index.tsx
1
import { useContext, useEffect, useState } from 'react'
2
import { useTranslation } from 'react-i18next'
3
import moment, { Moment } from 'moment'
4
import styled from 'styled-components'
5
import { useSessionStorage } from 'usehooks-ts'
6
import { FormControlLabel, Switch } from '@mui/material'
7
import Grid from '@mui/material/Unstable_Grid2' // Grid version 2
8
import CircularProgress from '@mui/material/CircularProgress'
9
import axios from 'axios'
10
import Typography from '@mui/material/Typography'
11
import Alert from '@mui/material/Alert'
12
import { PageContainer } from '../components/PageContainer'
13
import { Row } from '../components/Row'
14
import { Label } from '../components/Label'
15
import OperatorSelector from '../components/OperatorSelector'
16
import LineNumberSelector from '../components/LineSelector'
17
import { SearchContext } from '../../model/pageState'
18
import { Gap, GapsList } from '../../model/gaps'
19
import { getGapsAsync } from '../../api/gapsService'
20
import RouteSelector from '../components/RouteSelector'
21
import { NotFound } from '../components/NotFound'
22
import { getRoutesAsync } from '../../api/gtfsService'
23
import { DateSelector } from '../components/DateSelector'
24
import DisplayGapsPercentage from '../components/DisplayGapsPercentage'
25
import { INPUT_SIZE } from 'src/resources/sizes'
26

27
const Cell = styled.div`
4✔
28
  width: 120px;
29
`
30

31
const TitleCell = styled(Cell)`
4✔
32
  font-weight: bold;
33
`
34

35
const GapsPage = () => {
4✔
36
  const { t } = useTranslation()
140✔
37
  const { search, setSearch } = useContext(SearchContext)
140✔
38
  const { operatorId, lineNumber, timestamp, routes, routeKey } = search
140✔
39
  const [gaps, setGaps] = useState<GapsList>()
140✔
40
  const [routesIsLoading, setRoutesIsLoading] = useState(false)
140✔
41
  const [gapsIsLoading, setGapsIsLoading] = useState(false)
140✔
42
  const [onlyGapped, setOnlyGapped] = useSessionStorage('onlyGapped', false)
140✔
43

44
  function formatTime(time: Moment) {
45
    return time.format(t('time_format'))
1,164✔
46
  }
47

48
  function formatStatus(all: GapsList, gap: Gap) {
49
    if (!gap.siriTime) {
4,332✔
50
      return t('ride_missing')
128✔
51
    }
52
    if (gap.gtfsTime) {
4,204✔
53
      return t('ride_as_planned')
4,140✔
54
    }
55
    const hasTwinRide = all.some((g) => g.gtfsTime && g.siriTime && g.siriTime.isSame(gap.siriTime))
1,440✔
56
    if (hasTwinRide) {
64!
57
      return t('ride_duped')
×
58
    }
59
    return t('ride_extra')
64✔
60
  }
61

62
  function getGapsPercentage(gaps: GapsList | undefined): number | undefined {
63
    const ridesInTime = gaps?.filter((gap) => formatStatus([], gap) === t('ride_as_planned'))
3,168✔
64
    if (!gaps || !ridesInTime) return undefined
140✔
65
    const ridesInTimePercentage = (ridesInTime?.length / gaps?.length) * 100
44✔
66
    const allRidesPercentage = 100
44✔
67
    return allRidesPercentage - ridesInTimePercentage
44✔
68
  }
69

70
  useEffect(() => {
140✔
71
    const source = axios.CancelToken.source()
24✔
72
    if (operatorId && routes && routeKey && timestamp) {
24✔
73
      const selectedRoute = routes.find((route) => route.key === routeKey)
4✔
74
      if (!selectedRoute) {
4!
UNCOV
75
        return
×
76
      }
77
      setGapsIsLoading(true)
4✔
78
      getGapsAsync(
4✔
79
        moment(timestamp),
80
        moment(timestamp),
81
        operatorId,
82
        selectedRoute.lineRef,
83
        source.token,
84
      )
85
        .then(setGaps)
86
        .catch((err) => console.error(err.message))
×
87
        .finally(() => setGapsIsLoading(false))
4✔
88
    }
89
    return () => source.cancel()
24✔
90
  }, [operatorId, routeKey, timestamp])
91

92
  useEffect(() => {
140✔
93
    const controller = new AbortController()
18✔
94
    const signal = controller.signal
18✔
95
    if (!operatorId || operatorId === '0' || !lineNumber) {
18✔
96
      setSearch((current) => ({
14✔
97
        ...current,
98
        routes: undefined,
99
        routeKey: undefined,
100
      }))
101
      return
14✔
102
    }
103
    setRoutesIsLoading(true)
4✔
104
    getRoutesAsync(moment(timestamp), moment(timestamp), operatorId, lineNumber, signal)
4✔
105
      .then((routes) =>
106
        setSearch((current) =>
4✔
107
          search.lineNumber === lineNumber ? { ...current, routes: routes } : current,
4!
108
        ),
109
      )
110
      .catch((err) => console.error(err.message))
×
111
      .finally(() => setRoutesIsLoading(false))
4✔
112
    return () => controller.abort()
4✔
113
  }, [operatorId, lineNumber, timestamp, setSearch])
114

115
  const gapsPercentage = getGapsPercentage(gaps)
140✔
116

117
  return (
118
    <PageContainer>
119
      <Typography className="page-title" variant="h4">
120
        {t('gaps_page_title')}
121
      </Typography>
122
      <Alert severity="info" variant="outlined" icon={false}>
123
        {t('gaps_page_description')}
124
      </Alert>
125
      <Grid container spacing={2} sx={{ maxWidth: INPUT_SIZE }}>
126
        {/* choose date */}
127
        <Grid xs={4}>
128
          <Label text={t('choose_date')} />
129
        </Grid>
130
        <Grid xs={8}>
131
          <DateSelector
132
            time={moment(timestamp)}
133
            onChange={(ts) =>
134
              setSearch((current) => ({ ...current, timestamp: ts ? ts.valueOf() : 0 }))
×
135
            }
136
          />
137
        </Grid>
138
        {/* choose operator */}
139
        <Grid xs={4}>
140
          <Label text={t('choose_operator')} />
141
        </Grid>
142
        <Grid xs={8}>
143
          <OperatorSelector
144
            operatorId={operatorId}
145
            setOperatorId={(id) => setSearch((current) => ({ ...current, operatorId: id }))}
4✔
146
          />
147
        </Grid>
148
        {/* choose line */}
149
        <Grid xs={4}>
150
          <Label text={t('choose_line')} />
151
        </Grid>
152
        <Grid xs={8}>
153
          <LineNumberSelector
154
            lineNumber={lineNumber}
155
            setLineNumber={(number) => setSearch((current) => ({ ...current, lineNumber: number }))}
6✔
156
          />
157
        </Grid>
158
        {/* choose routes */}
159
        <Grid xs={12}>
160
          {routesIsLoading && (
140✔
161
            <Row>
162
              <Label text={t('loading_routes')} />
163
              <CircularProgress />
164
            </Row>
165
          )}
166
          {!routesIsLoading &&
320✔
167
            routes &&
168
            (routes.length === 0 ? (
56!
169
              <NotFound>{t('line_not_found')}</NotFound>
170
            ) : (
171
              <RouteSelector
172
                routes={routes}
173
                routeKey={routeKey}
174
                setRouteKey={(key) => setSearch((current) => ({ ...current, routeKey: key }))}
6✔
175
              />
176
            ))}
177
        </Grid>
178
        <Grid xs={12}>
179
          {gapsIsLoading && (
140✔
180
            <Row>
181
              <Label text={t('loading_gaps')} />
182
              <CircularProgress />
183
            </Row>
184
          )}
185
        </Grid>
186
      </Grid>
187
      {!gapsIsLoading && routeKey && routeKey !== '0' && (
300✔
188
        <>
189
          <FormControlLabel
190
            control={
191
              <Switch checked={onlyGapped} onChange={(e) => setOnlyGapped(e.target.checked)} />
4✔
192
            }
193
            label={t('checkbox_only_gaps')}
194
          />
195
          <DisplayGapsPercentage
196
            gapsPercentage={gapsPercentage}
197
            decentPercentage={5}
198
            terriblePercentage={20}
199
          />
200
          <Row>
201
            <TitleCell>{t('planned_time')}</TitleCell>
202
            <TitleCell>{t('planned_status')}</TitleCell>
203
          </Row>
204
          {gaps
205
            ?.filter((gap) => gap.gtfsTime || gap.siriTime)
1,440✔
206
            .filter((gap) => !onlyGapped || !gap.gtfsTime || !gap.siriTime)
1,440✔
207
            .sort((t1, t2) => {
208
              return Number((t1?.siriTime || t1?.gtfsTime)?.diff(t2?.siriTime || t2?.gtfsTime))
1,352✔
209
            })
210
            .map((gap, i) => (
211
              <Row key={i}>
212
                <Cell>{formatTime(gap.gtfsTime || gap.siriTime || moment())}</Cell>
1,184!
213
                <Cell>{formatStatus(gaps, gap)}</Cell>
214
              </Row>
215
            ))}
216
        </>
217
      )}
218
    </PageContainer>
219
  )
220
}
221

222
export default GapsPage
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc