aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modern/package.json1
-rw-r--r--modern/public/index.html2
-rw-r--r--modern/src/App.js4
-rw-r--r--modern/src/DeviceList.js4
-rw-r--r--modern/src/DevicePage.js14
-rw-r--r--modern/src/LoginPage.js22
-rw-r--r--modern/src/MainPage.js49
-rw-r--r--modern/src/MainToolbar.js11
-rw-r--r--modern/src/RouteReportPage.js19
-rw-r--r--modern/src/SocketController.js35
-rw-r--r--modern/src/common/formatter.js23
-rw-r--r--modern/src/reports/RouteReportPage.js168
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;