diff options
author | Anton Tananaev <anton.tananaev@gmail.com> | 2020-07-25 12:36:19 -0700 |
---|---|---|
committer | Anton Tananaev <anton.tananaev@gmail.com> | 2020-07-25 12:36:19 -0700 |
commit | d69bb2b2c3053c2c61e4e5d7029751debcfb0dd9 (patch) | |
tree | a8acdb87aea6c39ba1c0712186a2be1dadaff181 | |
parent | 94be29b98ef9ca509c38c2576dc56828a788937e (diff) | |
download | trackermap-web-d69bb2b2c3053c2c61e4e5d7029751debcfb0dd9.tar.gz trackermap-web-d69bb2b2c3053c2c61e4e5d7029751debcfb0dd9.tar.bz2 trackermap-web-d69bb2b2c3053c2c61e4e5d7029751debcfb0dd9.zip |
Implement simple route report
-rw-r--r-- | modern/package.json | 1 | ||||
-rw-r--r-- | modern/public/index.html | 2 | ||||
-rw-r--r-- | modern/src/App.js | 4 | ||||
-rw-r--r-- | modern/src/DeviceList.js | 4 | ||||
-rw-r--r-- | modern/src/DevicePage.js | 14 | ||||
-rw-r--r-- | modern/src/LoginPage.js | 22 | ||||
-rw-r--r-- | modern/src/MainPage.js | 49 | ||||
-rw-r--r-- | modern/src/MainToolbar.js | 11 | ||||
-rw-r--r-- | modern/src/RouteReportPage.js | 19 | ||||
-rw-r--r-- | modern/src/SocketController.js | 35 | ||||
-rw-r--r-- | modern/src/common/formatter.js | 23 | ||||
-rw-r--r-- | modern/src/reports/RouteReportPage.js | 168 |
12 files changed, 262 insertions, 90 deletions
diff --git a/modern/package.json b/modern/package.json index d06ac59c..4d4765c1 100644 --- a/modern/package.json +++ b/modern/package.json @@ -8,6 +8,7 @@ "@material-ui/icons": "^4.9.1", "@reduxjs/toolkit": "^1.2.5", "mapbox-gl": "^1.8.1", + "moment": "^2.27.0", "ol": "^6.2.1", "ol-mapbox-style": "^6.0.1", "react": "^16.13.0", diff --git a/modern/public/index.html b/modern/public/index.html index 29800eb2..ecded7c1 100644 --- a/modern/public/index.html +++ b/modern/public/index.html @@ -12,6 +12,6 @@ <noscript> You need to enable JavaScript to run this app. </noscript> - <div id="root"></div> + <div id="root" style="height: 100vh;"></div> </body> </html> diff --git a/modern/src/App.js b/modern/src/App.js index 7921a2c8..f45c64de 100644 --- a/modern/src/App.js +++ b/modern/src/App.js @@ -3,13 +3,15 @@ import { Switch, Route } from 'react-router-dom' import CssBaseline from '@material-ui/core/CssBaseline'; import MainPage from './MainPage'; import LoginPage from './LoginPage'; -import RouteReportPage from './RouteReportPage'; +import RouteReportPage from './reports/RouteReportPage'; import DevicePage from './DevicePage'; +import SocketController from './SocketController'; const App = () => { return ( <> <CssBaseline /> + <SocketController /> <Switch> <Route exact path='/' component={MainPage} /> <Route exact path='/login' component={LoginPage} /> diff --git a/modern/src/DeviceList.js b/modern/src/DeviceList.js index 5e09041a..66b94063 100644 --- a/modern/src/DeviceList.js +++ b/modern/src/DeviceList.js @@ -23,8 +23,8 @@ import RemoveDialog from './RemoveDialog'; const useStyles = makeStyles(theme => ({ list: { - maxHeight: '100%', - overflow: 'auto', + maxHeight: '100%', + overflow: 'auto', }, fab: { position: 'absolute', diff --git a/modern/src/DevicePage.js b/modern/src/DevicePage.js index c9362deb..ee525492 100644 --- a/modern/src/DevicePage.js +++ b/modern/src/DevicePage.js @@ -72,28 +72,28 @@ const DevicePage = () => { <form> {(!id || device) && <TextField - margin="normal" + margin='normal' fullWidth defaultValue={device && device.name} onChange={(event) => setName(event.target.value)} label={t('sharedName')} - variant="filled" /> + variant='filled' /> } {(!id || device) && <TextField - margin="normal" + margin='normal' fullWidth defaultValue={device && device.uniqueId} onChange={(event) => setUniqueId(event.target.value)} label={t('deviceIdentifier')} - variant="filled" /> + variant='filled' /> } - <FormControl fullWidth margin="normal"> + <FormControl fullWidth margin='normal'> <div className={classes.buttons}> - <Button type="button" color="primary" variant="outlined" onClick={() => history.goBack()}> + <Button type='button' color='primary' variant='outlined' onClick={() => history.goBack()}> {t('sharedCancel')} </Button> - <Button type="button" color="primary" variant="contained" onClick={handleSave}> + <Button type='button' color='primary' variant='contained' onClick={handleSave}> {t('sharedSave')} </Button> </div> diff --git a/modern/src/LoginPage.js b/modern/src/LoginPage.js index 68ac31f0..429a6e6f 100644 --- a/modern/src/LoginPage.js +++ b/modern/src/LoginPage.js @@ -79,40 +79,40 @@ const LoginPage = () => { return ( <main className={classes.root}> <Paper className={classes.paper}> - <img className={classes.logo} src="/logo.svg" alt="Traccar" /> + <img className={classes.logo} src='/logo.svg' alt='Traccar' /> <form onSubmit={handleLogin}> <TextField - margin="normal" + margin='normal' required fullWidth error={failed} label={t('userEmail')} - name="email" + name='email' value={email} - autoComplete="email" + autoComplete='email' autoFocus onChange={handleEmailChange} helperText={failed && 'Invalid username or password'} /> <TextField - margin="normal" + margin='normal' required fullWidth error={failed} label={t('userPassword')} - name="password" + name='password' value={password} - type="password" - autoComplete="current-password" + type='password' + autoComplete='current-password' onChange={handlePasswordChange} /> - <FormControl fullWidth margin="normal"> + <FormControl fullWidth margin='normal'> <div className={classes.buttons}> - <Button type="button" variant="contained" disabled onClick={handleRegister}> + <Button type='button' variant='contained' disabled onClick={handleRegister}> {t('loginRegister')} </Button> - <Button type="submit" variant="contained" color="primary" disabled={!email || !password}> + <Button type='submit' variant='contained' color='primary' disabled={!email || !password}> {t('loginLogin')} </Button> </div> diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index bc792470..1ab151fa 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -1,72 +1,53 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { sessionActions } from './store'; -import { useHistory } from 'react-router-dom'; +import React from 'react'; +import { useSelector } from 'react-redux'; import { isWidthUp, makeStyles, withWidth } from '@material-ui/core'; import Drawer from '@material-ui/core/Drawer'; import ContainerDimensions from 'react-container-dimensions'; import LinearProgress from '@material-ui/core/LinearProgress'; - import DeviceList from './DeviceList'; import MainMap from './MainMap'; import MainToobar from './MainToolbar'; -import SocketController from './SocketController'; const useStyles = makeStyles(theme => ({ root: { - height: "100vh", - display: "flex", - flexDirection: "column" + height: '100%', + display: 'flex', + flexDirection: 'column', }, content: { flexGrow: 1, - overflow: "hidden", - display: "flex", - flexDirection: "row", + overflow: 'hidden', + display: 'flex', + flexDirection: 'row', [theme.breakpoints.down('xs')]: { - flexDirection: "column-reverse" + flexDirection: 'column-reverse', } }, drawerPaper: { position: 'relative', [theme.breakpoints.up('sm')]: { - width: 350 + width: 350, }, [theme.breakpoints.down('xs')]: { - height: 250 + height: 250, } }, mapContainer: { - flexGrow: 1 - } + flexGrow: 1, + }, })); const MainPage = ({ width }) => { - const dispatch = useDispatch(); const authenticated = useSelector(state => state.session.authenticated); const classes = useStyles(); - const history = useHistory(); - - useEffect(() => { - if (!authenticated) { - fetch('/api/session').then(response => { - if (response.ok) { - dispatch(sessionActions.authenticated(true)); - } else { - history.push('/login'); - } - }); - } - }, [authenticated]); return !authenticated ? (<LinearProgress />) : ( <div className={classes.root}> - <SocketController /> <MainToobar /> <div className={classes.content}> <Drawer - anchor={isWidthUp('sm', width) ? "left" : "bottom"} - variant="permanent" + anchor={isWidthUp('sm', width) ? 'left' : 'bottom'} + variant='permanent' classes={{ paper: classes.drawerPaper }}> <DeviceList /> </Drawer> diff --git a/modern/src/MainToolbar.js b/modern/src/MainToolbar.js index f77868df..5994a388 100644 --- a/modern/src/MainToolbar.js +++ b/modern/src/MainToolbar.js @@ -87,10 +87,13 @@ const MainToolbar = () => { </ListItem> </List> <Divider /> - <List subheader={<ListSubheader> - {t('reportTitle')} - </ListSubheader>}> - <ListItem button disabled onClick={() => { history.push('/reports/route') }}> + <List + subheader={ + <ListSubheader> + {t('reportTitle')} + </ListSubheader> + }> + <ListItem button onClick={() => { history.push('/reports/route') }}> <ListItemIcon> <BarChartIcon /> </ListItemIcon> diff --git a/modern/src/RouteReportPage.js b/modern/src/RouteReportPage.js deleted file mode 100644 index 6bbf01ec..00000000 --- a/modern/src/RouteReportPage.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import MainToobar from './MainToolbar'; -import withStyles from '@material-ui/core/styles/withStyles'; -import withWidth from '@material-ui/core/withWidth'; -import { useHistory } from 'react-router-dom'; - -const styles = theme => ({}); - -const RouteReportPage = () => { - const history = useHistory(); - - return ( - <div> - <MainToobar history={history} /> - </div> - ); -} - -export default withWidth()(withStyles(styles)(RouteReportPage)); diff --git a/modern/src/SocketController.js b/modern/src/SocketController.js index 13ff0a9d..94be52a6 100644 --- a/modern/src/SocketController.js +++ b/modern/src/SocketController.js @@ -1,7 +1,8 @@ import { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { connect } from 'react-redux'; -import { positionsActions, devicesActions } from './store'; +import { positionsActions, devicesActions, sessionActions } from './store'; +import { useHistory } from 'react-router-dom'; const displayNotifications = events => { if ("Notification" in window) { @@ -22,6 +23,8 @@ const displayNotifications = events => { const SocketController = () => { const dispatch = useDispatch(); + const history = useHistory(); + const authenticated = useSelector(state => state.session.authenticated); const connectSocket = () => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; @@ -46,15 +49,25 @@ const SocketController = () => { } useEffect(() => { - fetch('/api/devices').then(response => { - if (response.ok) { - response.json().then(devices => { - dispatch(devicesActions.update(devices)); - }); - } - connectSocket(); - }); - }, []); + if (authenticated) { + fetch('/api/devices').then(response => { + if (response.ok) { + response.json().then(devices => { + dispatch(devicesActions.update(devices)); + }); + } + connectSocket(); + }); + } else { + fetch('/api/session').then(response => { + if (response.ok) { + dispatch(sessionActions.authenticated(true)); + } else { + history.push('/login'); + } + }); + } + }, [authenticated]); return null; } diff --git a/modern/src/common/formatter.js b/modern/src/common/formatter.js new file mode 100644 index 00000000..ce46c6cf --- /dev/null +++ b/modern/src/common/formatter.js @@ -0,0 +1,23 @@ +import moment from 'moment'; + +const formatValue = (key, value) => { + switch (key) { + case 'fixTime': + return moment(value).format('LLL'); + case 'latitude': + case 'longitude': + return value.toFixed(5); + case 'speed': + return value.toFixed(1); + default: + return value; + } +} + +export default (object, key) => { + if (object != null && typeof object == 'object') { + return formatValue(key, object[key]); + } else { + return formatValue(key, object); + } +}; diff --git a/modern/src/reports/RouteReportPage.js b/modern/src/reports/RouteReportPage.js new file mode 100644 index 00000000..eb0ccc55 --- /dev/null +++ b/modern/src/reports/RouteReportPage.js @@ -0,0 +1,168 @@ +import React, { useState } from 'react'; +import MainToobar from '../MainToolbar'; +import { useHistory } from 'react-router-dom'; +import { Grid, TableContainer, Table, TableRow, TableCell, TableHead, TableBody, Paper, makeStyles, FormControl, InputLabel, Select, MenuItem, Button, TextField } from '@material-ui/core'; +import t from '../common/localization'; +import { useSelector } from 'react-redux'; +import moment from 'moment'; +import formatter from '../common/formatter'; + +const useStyles = makeStyles(theme => ({ + root: { + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + content: { + flex: 1, + overflow: 'auto', + padding: theme.spacing(2), + }, + form: { + padding: theme.spacing(1, 2, 2), + }, +})); + +const RouteReportPage = () => { + const history = useHistory(); + const classes = useStyles(); + const devices = useSelector(state => Object.values(state.devices.items)); + const [deviceId, setDeviceId] = useState(); + const [period, setPeriod] = useState('today'); + const [from, setFrom] = useState(moment().subtract(1, 'hour')); + const [to, setTo] = useState(moment()); + const [data, setData] = useState([]); + + const handleShow = () => { + let selectedFrom; + let selectedTo; + switch (period) { + case 'today': + selectedFrom = moment().startOf('day'); + selectedTo = moment().endOf('day'); + break; + case 'yesterday': + selectedFrom = moment().subtract(1, 'day').startOf('day'); + selectedTo = moment().subtract(1, 'day').endOf('day'); + break; + case 'thisWeek': + selectedFrom = moment().startOf('week'); + selectedTo = moment().endOf('week'); + break; + case 'previousWeek': + selectedFrom = moment().subtract(1, 'week').startOf('week'); + selectedTo = moment().subtract(1, 'week').endOf('week'); + break; + case 'thisMonth': + selectedFrom = moment().startOf('month'); + selectedTo = moment().endOf('month'); + break; + case 'previousMonth': + selectedFrom = moment().subtract(1, 'month').startOf('month'); + selectedTo = moment().subtract(1, 'month').endOf('month'); + break; + default: + selectedFrom = from; + selectedTo = to; + break; + } + const query = new URLSearchParams({ + deviceId, + from: selectedFrom.toISOString(), + to: selectedTo.toISOString(), + }); + fetch(`/api/reports/route?${query.toString()}`, { headers: { 'Accept': 'application/json' } }) + .then(response => { + if (response.ok) { + response.json().then(setData); + } + }); + } + + return ( + <div className={classes.root}> + <MainToobar history={history} /> + <div className={classes.content}> + <Grid container spacing={2}> + <Grid item xs={12} md={3} lg={2}> + <Paper className={classes.form}> + <FormControl variant='filled' margin='normal' fullWidth> + <InputLabel>{t('reportDevice')}</InputLabel> + <Select value={deviceId} onChange={(e) => setDeviceId(e.target.value)}> + {devices.map((device) => ( + <MenuItem value={device.id}>{device.name}</MenuItem> + ))} + </Select> + </FormControl> + <FormControl variant='filled' margin='normal' fullWidth> + <InputLabel>{t('reportPeriod')}</InputLabel> + <Select value={period} onChange={(e) => setPeriod(e.target.value)}> + <MenuItem value='today'>{t('reportToday')}</MenuItem> + <MenuItem value='yesterday'>{t('reportYesterday')}</MenuItem> + <MenuItem value='thisWeek'>{t('reportThisWeek')}</MenuItem> + <MenuItem value='previousWeek'>{t('reportPreviousWeek')}</MenuItem> + <MenuItem value='thisMonth'>{t('reportThisMonth')}</MenuItem> + <MenuItem value='previousMonth'>{t('reportPreviousMonth')}</MenuItem> + <MenuItem value='custom'>{t('reportCustom')}</MenuItem> + </Select> + </FormControl> + {period === 'custom' && + <TextField + margin='normal' + variant='filled' + label={t('reportFrom')} + type='datetime-local' + value={from.format(moment.HTML5_FMT.DATETIME_LOCAL)} + onChange={(e) => setFrom(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + fullWidth /> + } + {period === 'custom' && + <TextField + margin='normal' + variant='filled' + label={t('reportTo')} + type='datetime-local' + value={to.format(moment.HTML5_FMT.DATETIME_LOCAL)} + onChange={(e) => setTo(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + fullWidth /> + } + <FormControl margin='normal' fullWidth> + <Button type='button' color='primary' variant='contained' disabled={!deviceId} onClick={handleShow}> + {t('reportShow')} + </Button> + </FormControl> + </Paper> + </Grid> + <Grid item xs={12} md={9} lg={10}> + <TableContainer component={Paper}> + <Table> + <TableHead> + <TableRow> + <TableCell>{t('positionFixTime')}</TableCell> + <TableCell>{t('positionLatitude')}</TableCell> + <TableCell>{t('positionLongitude')}</TableCell> + <TableCell>{t('positionSpeed')}</TableCell> + <TableCell>{t('positionAddress')}</TableCell> + </TableRow> + </TableHead> + <TableBody> + {data.map((item) => ( + <TableRow key={item.id}> + <TableCell>{formatter(item, 'fixTime')}</TableCell> + <TableCell>{formatter(item, 'latitude')}</TableCell> + <TableCell>{formatter(item, 'longitude')}</TableCell> + <TableCell>{formatter(item, 'speed')}</TableCell> + <TableCell>{formatter(item, 'address')}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </TableContainer> + </Grid> + </Grid> + </div> + </div> + ); +} + +export default RouteReportPage; |