aboutsummaryrefslogtreecommitdiff
path: root/modern/src
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src')
-rw-r--r--modern/src/App.js4
-rw-r--r--modern/src/CommandsPage.js121
-rw-r--r--modern/src/DevicesList.js141
-rw-r--r--modern/src/LocalizationProvider.js108
-rw-r--r--modern/src/MainPage.js60
-rw-r--r--modern/src/MainToolbar.js21
-rw-r--r--modern/src/StartPage.js7
-rw-r--r--modern/src/common/deviceCategories.js1
-rw-r--r--modern/src/common/formatter.js24
-rw-r--r--modern/src/common/selectors.js4
-rw-r--r--modern/src/components/BottomMenu.js10
-rw-r--r--modern/src/components/registration/LoginForm.js11
-rw-r--r--modern/src/index.js2
-rw-r--r--modern/src/map/Map.js37
-rw-r--r--modern/src/map/PositionsMap.js37
-rw-r--r--modern/src/map/SelectedDeviceMap.js4
-rw-r--r--modern/src/map/StatusView.js109
-rw-r--r--modern/src/map/mapStyles.js8
-rw-r--r--modern/src/map/mapUtil.js8
-rw-r--r--modern/src/reports/ReplayPage.js60
-rw-r--r--modern/src/reports/ReportFilter.js12
-rw-r--r--modern/src/setupProxy.js6
-rw-r--r--modern/src/store/devices.js18
-rw-r--r--modern/src/store/index.js3
-rw-r--r--modern/src/store/ui.js16
-rw-r--r--modern/src/theme/index.js4
26 files changed, 505 insertions, 331 deletions
diff --git a/modern/src/App.js b/modern/src/App.js
index a53ffc6..95ed1c2 100644
--- a/modern/src/App.js
+++ b/modern/src/App.js
@@ -26,6 +26,7 @@ import DriversPage from './settings/DriversPage';
import DriverPage from './settings/DriverPage';
import CalendarsPage from './settings/CalendarsPage';
import CalendarPage from './settings/CalendarPage';
+import CommandsPage from './CommandsPage';
import ComputedAttributesPage from './settings/ComputedAttributesPage';
import ComputedAttributePage from './settings/ComputedAttributePage';
import MaintenancesPage from './settings/MaintenancesPage';
@@ -59,10 +60,11 @@ const App = () => {
{!initialized ? (<LinearProgress />) : (
<Switch>
<Route exact path="/" component={MainPage} />
- <Route exact path="/replay" component={ReplayPage} />
+ <Route exact path="/replay/:id?" component={ReplayPage} />
<Route exact path="/position/:id?" component={PositionPage} />
<Route exact path="/user/:id?" component={UserPage} />
<Route exact path="/device/:id?" component={DevicePage} />
+ <Route exact path="/device/:id?/commands" component={CommandsPage} />
<Route exact path="/geofence/:id?" component={GeofencePage} />
<Route exact path="/geofences" component={GeofencesPage} />
<Route exact path="/settings/notifications" component={NotificationsPage} />
diff --git a/modern/src/CommandsPage.js b/modern/src/CommandsPage.js
new file mode 100644
index 0000000..1458b43
--- /dev/null
+++ b/modern/src/CommandsPage.js
@@ -0,0 +1,121 @@
+import React, { useState } from 'react';
+import { useHistory, useParams } from 'react-router-dom';
+import {
+ makeStyles, Typography, Container, Card, CardContent, RadioGroup, Radio, FormControl, FormControlLabel, Button
+} from '@material-ui/core';
+
+import MainToolbar from './MainToolbar';
+import { useEffectAsync } from './reactHelper';
+import { useTranslation } from './LocalizationProvider';
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ marginTop: theme.spacing(2),
+ marginBottom: theme.spacing(2),
+ },
+ buttons: {
+ display: 'flex',
+ justifyContent: 'space-evenly',
+ '& > *': {
+ flexBasis: '33%',
+ },
+ },
+}));
+
+const CommandsPage = () => {
+ const classes = useStyles();
+ const { id } = useParams();
+ const t = useTranslation();
+ const history = useHistory();
+
+ const [device, setDevice] = useState();
+ const [commands, setCommands] = useState([]);
+ const [selectedCommand, setSelectedCommand] = useState();
+
+ useEffectAsync(async () => {
+ if (id) {
+ let device = undefined;
+
+ const response = await fetch(`/api/devices?id=${id}`, {
+ headers: {
+ Accept: 'application/json'
+ },
+ });
+ if (response.ok) {
+ const items = await response.json();
+ device = items[0];
+ setDevice(items[0]);
+ } else {
+ setDevice({});
+ }
+
+ if (device) {
+ const response = await fetch(`/api/commands/send?deviceId=${device.id}`, {
+ headers: {
+ Accept: 'application/json'
+ },
+ });
+ if (response.ok) {
+ const items = await response.json();
+ setCommands(items);
+ } else {
+ setCommands([]);
+ }
+ }
+ }
+ }, [id]);
+
+ const handleSend = async () => {
+ const response = await fetch(`/api/commands/send`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ 'id': selectedCommand,
+ 'deviceId': device.id,
+ }),
+ });
+
+ if (response.ok) {
+ history.goBack();
+ } else {
+ console.log ('response!', response);
+ }
+ };
+
+ return (
+ <>
+ <MainToolbar />
+ <Container maxWidth="sm" className={classes.root}>
+ <Card>
+ {device && (
+ <>
+ <CardContent>
+ <Typography gutterBottom variant="h5">{t('commandSend')}</Typography>
+ <Typography variant="body2" color="text.secondary">{device.name}</Typography>
+ {commands && (
+ <FormControl fullWidth aria-label="command">
+ <RadioGroup onChange={(event) => setSelectedCommand(event.target.value) }>
+ {commands.map (command => (
+ <FormControlLabel value={command.id.toString()} control={<Radio />} label={command.description} />
+ ))}
+ </RadioGroup>
+ <div className={classes.buttons}>
+ <Button type="button" color="primary" variant="outlined" onClick={() => history.goBack()}>
+ {t('sharedCancel')}
+ </Button>
+ <Button type="button" color="primary" variant="contained" onClick={handleSend}>
+ {t('commandSend')}
+ </Button>
+ </div>
+ </FormControl>
+ )}
+ </CardContent>
+ </>
+ )}
+ </Card>
+ </Container>
+ </>
+ );
+}
+
+export default CommandsPage;
diff --git a/modern/src/DevicesList.js b/modern/src/DevicesList.js
index e06fd58..b98956c 100644
--- a/modern/src/DevicesList.js
+++ b/modern/src/DevicesList.js
@@ -1,7 +1,6 @@
import React, { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from '@material-ui/core/styles';
-import Avatar from '@material-ui/core/Avatar';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
@@ -14,11 +13,19 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import BatteryFullIcon from '@material-ui/icons/BatteryFull';
import { ReactComponent as IgnitionIcon } from '../public/images/ignition.svg';
-import { devicesActions } from './store';
+import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
+import PersonIcon from '@material-ui/icons/Person';
+import SpeedIcon from '@material-ui/icons/Speed';
+import CalendarTodayIcon from '@material-ui/icons/CalendarToday';
+import LockIcon from '@material-ui/icons/Lock';
+import LockOpenIcon from '@material-ui/icons/LockOpen';
+
+import { devicesActions, uiActions } from './store';
import EditCollectionView from './EditCollectionView';
import { useEffectAsync } from './reactHelper';
-import { formatPosition } from './common/formatter';
-import { getDevices, getPosition } from './common/selectors';
+import { formatPosition, formatSpeed, formatHours } from './common/formatter';
+import { useAttributePreference } from './common/preferences';
+import { getDevices, getFilteredDevices, getFilterTerm, getPosition } from './common/selectors';
import { useTranslation } from './LocalizationProvider';
const useStyles = makeStyles((theme) => ({
@@ -30,15 +37,21 @@ const useStyles = makeStyles((theme) => ({
margin: theme.spacing(1.5, 0),
},
icon: {
- width: '25px',
- height: '25px',
- filter: 'brightness(0) invert(1)',
+ width: '40px',
+ height: '40px',
+ },
+ statusIcon: {
+ paddingRight: '5px',
},
listItem: {
backgroundColor: 'white',
'&:hover': {
backgroundColor: 'white',
},
+ height: '150px',
+ },
+ listItemSecondary: {
+ fontSize: '0.92rem',
},
batteryText: {
fontSize: '0.75rem',
@@ -59,18 +72,6 @@ const useStyles = makeStyles((theme) => ({
},
}));
-const getStatusColor = (status) => {
- switch (status) {
- case 'online':
- return 'green';
- case 'offline':
- return 'red';
- case 'unknown':
- default:
- return 'gray';
- }
-};
-
const getBatteryStatus = (batteryLevel) => {
if (batteryLevel >= 70) {
return 'green';
@@ -86,39 +87,91 @@ const DeviceRow = ({ data, index, style }) => {
const dispatch = useDispatch();
const t = useTranslation();
+ const speedUnit = useAttributePreference('speedUnit');
+
const { items } = data;
const item = items[index];
const position = useSelector(getPosition(item.id));
const showIgnition = position?.attributes.hasOwnProperty('ignition') && position.attributes.ignition;
+ const statusColor = () => {
+ if (position && position.speed >= 2) {
+ return 'primary';
+ } else {
+ return 'error';
+ }
+ };
+
return (
<div style={style}>
- <ListItem button key={item.id} className={classes.listItem} onClick={() => dispatch(devicesActions.select(item))}>
+ <ListItem button key={item.id} className={classes.listItem} onClick={() => {
+ dispatch(devicesActions.select(item));
+ dispatch(uiActions.setCollapsed(true));
+ setTimeout(() => {
+ dispatch(devicesActions.unselect());
+ }, 1000);
+ }}>
+ {/* Avatar */}
<ListItemAvatar>
- <Avatar>
- <img className={classes.icon} src={`images/icon/${item.category || 'default'}.svg`} alt="" />
- </Avatar>
+ <img className={classes.icon} src={`images/icon/${(item.category || 'default').toLowerCase()}.png`} alt="" />
</ListItemAvatar>
- <ListItemText primary={item.name} secondary={item.status} classes={{ secondary: classes[getStatusColor(item.status)] }} />
+
+ {/* Status icon */}
+ <ListItemText primary={
+ <>
+ <FiberManualRecordIcon fontSize="inherit" color={statusColor()} classes={{ colorPrimary: classes.green }} />
+ {position && position.attributes.out1 == false && (
+ <LockOpenIcon fontSize="inherit" color="primary" classes={{ colorPrimary: classes.green }} />
+ )}
+ {position && position.attributes.out1 == true && (
+ <LockIcon fontSize="inherit" color="error" />
+ )}
+ {` ${item.name}`}
+ </>} secondary={(
+ <>
+ {/* Contact */}
+ {item.contact && (
+ <>
+ <PersonIcon fontSize="inherit" /> {item.contact}<br />
+ </>
+ )}
+ {position && (
+ <>
+ {/* Speed */}
+ <SpeedIcon fontSize="inherit" /> {formatSpeed(position.speed, speedUnit, t)}<br />
+ {/* Datetime */}
+ <CalendarTodayIcon fontSize="inherit" /> {formatPosition(position, 'fixTime', t)}
+ {/* Hours */}
+ {item.category
+ && (item.category.toLowerCase() === 'backhoe' || item.category.toLowerCase() === 'tractor' )
+ && position.attributes.hours
+ && position.attributes.hours > 1
+ && ` (${formatHours(position.attributes.hours, t)})`}
+ </>
+ )}
+ </>
+ )} classes={{ secondary: classes.listItemSecondary }} />
<ListItemSecondaryAction className={classes.indicators}>
{position && (
- <Grid container direction="row" alignItems="center" alignContent="center" spacing={2}>
- {showIgnition && (
- <Grid item>
- <SvgIcon component={IgnitionIcon} />
+ <Grid container direction="row" alignItems="center" alignContent="center" spacing={2}>
+ {/* Ignition */}
+ {showIgnition && (
+ <Grid item>
+ <SvgIcon component={IgnitionIcon} />
+ </Grid>
+ )}
+ {/* Battery level */}
+ {position.attributes.hasOwnProperty('batteryLevel') && (
+ <Grid item container xs alignItems="center" alignContent="center">
+ <Grid item>
+ <span className={classes.batteryText}>{formatPosition(position.attributes.batteryLevel, 'batteryLevel', t)}</span>
+ </Grid>
+ <Grid item>
+ <BatteryFullIcon className={classes[getBatteryStatus(position.attributes.batteryLevel)]} />
+ </Grid>
+ </Grid>
+ )}
</Grid>
- )}
- {position.attributes.hasOwnProperty('batteryLevel') && (
- <Grid item container xs alignItems="center" alignContent="center">
- <Grid item>
- <span className={classes.batteryText}>{formatPosition(position.attributes.batteryLevel, 'batteryLevel', t)}</span>
- </Grid>
- <Grid item>
- <BatteryFullIcon className={classes[getBatteryStatus(position.attributes.batteryLevel)]} />
- </Grid>
- </Grid>
- )}
- </Grid>
)}
</ListItemSecondaryAction>
</ListItem>
@@ -131,7 +184,11 @@ const DeviceView = ({ updateTimestamp, onMenuClick }) => {
const dispatch = useDispatch();
const listInnerEl = useRef(null);
- const items = useSelector(getDevices);
+ const filterTerm = useSelector(getFilterTerm);
+ const filteredItems = useSelector(getFilteredDevices);
+ const unfilteredItems = useSelector(getDevices);
+
+ const items = (filterTerm.length > 0 ? filteredItems : unfilteredItems).sort((a, b) => a.name.localeCompare(b.name));
if (listInnerEl.current) {
listInnerEl.current.className = classes.listInner;
@@ -153,7 +210,7 @@ const DeviceView = ({ updateTimestamp, onMenuClick }) => {
height={height}
itemCount={items.length}
itemData={{ items, onMenuClick }}
- itemSize={72}
+ itemSize={150}
overscanCount={10}
innerRef={listInnerEl}
>
diff --git a/modern/src/LocalizationProvider.js b/modern/src/LocalizationProvider.js
index cc23d72..6d8605b 100644
--- a/modern/src/LocalizationProvider.js
+++ b/modern/src/LocalizationProvider.js
@@ -1,116 +1,12 @@
import React, { createContext, useContext } from 'react';
import usePersistedState from './common/usePersistedState';
-import af from '../../web/l10n/af.json';
-import ar from '../../web/l10n/ar.json';
-import az from '../../web/l10n/az.json';
-import bg from '../../web/l10n/bg.json';
-import bn from '../../web/l10n/bn.json';
-import cs from '../../web/l10n/cs.json';
-import da from '../../web/l10n/da.json';
-import de from '../../web/l10n/de.json';
-import el from '../../web/l10n/el.json';
import en from '../../web/l10n/en.json';
import es from '../../web/l10n/es.json';
-import fa from '../../web/l10n/fa.json';
-import fi from '../../web/l10n/fi.json';
-import fr from '../../web/l10n/fr.json';
-import he from '../../web/l10n/he.json';
-import hi from '../../web/l10n/hi.json';
-import hr from '../../web/l10n/hr.json';
-import hu from '../../web/l10n/hu.json';
-import id from '../../web/l10n/id.json';
-import it from '../../web/l10n/it.json';
-import ja from '../../web/l10n/ja.json';
-import ka from '../../web/l10n/ka.json';
-import kk from '../../web/l10n/kk.json';
-import km from '../../web/l10n/km.json';
-import ko from '../../web/l10n/ko.json';
-import lo from '../../web/l10n/lo.json';
-import lt from '../../web/l10n/lt.json';
-import lv from '../../web/l10n/lv.json';
-import ml from '../../web/l10n/ml.json';
-import mn from '../../web/l10n/mn.json';
-import ms from '../../web/l10n/ms.json';
-import nb from '../../web/l10n/nb.json';
-import ne from '../../web/l10n/ne.json';
-import nl from '../../web/l10n/nl.json';
-import nn from '../../web/l10n/nn.json';
-import pl from '../../web/l10n/pl.json';
-import pt from '../../web/l10n/pt.json';
-import ptBR from '../../web/l10n/pt_BR.json';
-import ro from '../../web/l10n/ro.json';
-import ru from '../../web/l10n/ru.json';
-import si from '../../web/l10n/si.json';
-import sk from '../../web/l10n/sk.json';
-import sl from '../../web/l10n/sl.json';
-import sq from '../../web/l10n/sq.json';
-import sr from '../../web/l10n/sr.json';
-import sv from '../../web/l10n/sv.json';
-import ta from '../../web/l10n/ta.json';
-import th from '../../web/l10n/th.json';
-import tr from '../../web/l10n/tr.json';
-import uk from '../../web/l10n/uk.json';
-import uz from '../../web/l10n/uz.json';
-import vi from '../../web/l10n/vi.json';
-import zh from '../../web/l10n/zh.json';
-import zhTW from '../../web/l10n/zh_TW.json';
const languages = {
- af: { data: af, name: 'Afrikaans' },
- ar: { data: ar, name: 'العربية' },
- az: { data: az, name: 'Azərbaycanca' },
- bg: { data: bg, name: 'Български' },
- bn: { data: bn, name: 'বাংলা' },
- cs: { data: cs, name: 'Čeština' },
- de: { data: de, name: 'Deutsch' },
- da: { data: da, name: 'Dansk' },
- el: { data: el, name: 'Ελληνικά' },
en: { data: en, name: 'English' },
es: { data: es, name: 'Español' },
- fa: { data: fa, name: 'فارسی' },
- fi: { data: fi, name: 'Suomi' },
- fr: { data: fr, name: 'Français' },
- he: { data: he, name: 'עברית' },
- hi: { data: hi, name: 'हिन्दी' },
- hr: { data: hr, name: 'Hrvatski' },
- hu: { data: hu, name: 'Magyar' },
- id: { data: id, name: 'Bahasa Indonesia' },
- it: { data: it, name: 'Italiano' },
- ja: { data: ja, name: '日本語' },
- ka: { data: ka, name: 'ქართული' },
- kk: { data: kk, name: 'Қазақша' },
- ko: { data: ko, name: '한국어' },
- km: { data: km, name: 'ភាសាខ្មែរ' },
- lo: { data: lo, name: 'ລາວ' },
- lt: { data: lt, name: 'Lietuvių' },
- lv: { data: lv, name: 'Latviešu' },
- ml: { data: ml, name: 'മലയാളം' },
- mn: { data: mn, name: 'Монгол хэл' },
- ms: { data: ms, name: 'بهاس ملايو' },
- nb: { data: nb, name: 'Norsk bokmål' },
- ne: { data: ne, name: 'नेपाली' },
- nl: { data: nl, name: 'Nederlands' },
- nn: { data: nn, name: 'Norsk nynorsk' },
- pl: { data: pl, name: 'Polski' },
- pt: { data: pt, name: 'Português' },
- ptBR: { data: ptBR, name: 'Português (Brasil)' },
- ro: { data: ro, name: 'Română' },
- ru: { data: ru, name: 'Русский' },
- si: { data: si, name: 'සිංහල' },
- sk: { data: sk, name: 'Slovenčina' },
- sl: { data: sl, name: 'Slovenščina' },
- sq: { data: sq, name: 'Shqipëria' },
- sr: { data: sr, name: 'Srpski' },
- sv: { data: sv, name: 'Svenska' },
- ta: { data: ta, name: 'தமிழ்' },
- th: { data: th, name: 'ไทย' },
- tr: { data: tr, name: 'Türkçe' },
- uk: { data: uk, name: 'Українська' },
- uz: { data: uz, name: 'Oʻzbekcha' },
- vi: { data: vi, name: 'Tiếng Việt' },
- zh: { data: zh, name: '中文' },
- zhTW: { data: zhTW, name: '中文 (Taiwan)' },
};
const getDefaultLanguage = () => {
@@ -131,12 +27,12 @@ const getDefaultLanguage = () => {
}
}
}
- return 'en';
+ return 'es';
};
const LocalizationContext = createContext({
languages,
- language: 'en',
+ language: 'es',
setLanguage: () => {},
});
diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js
index 7fd4315..a118f17 100644
--- a/modern/src/MainPage.js
+++ b/modern/src/MainPage.js
@@ -1,8 +1,11 @@
-import React, { useState, useEffect } from 'react';
+import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import {
makeStyles, Paper, Toolbar, TextField, IconButton, Button,
} from '@material-ui/core';
+import { useDispatch, useSelector } from 'react-redux';
+import { devicesActions } from './store/devices';
+import { uiActions } from './store/ui';
import { useTheme } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
@@ -22,7 +25,6 @@ import BottomMenu from './components/BottomMenu';
import { useTranslation } from './LocalizationProvider';
import PoiMap from './map/PoiMap';
import MapPadding from './map/MapPadding';
-
const useStyles = makeStyles((theme) => ({
root: {
height: '100vh',
@@ -34,20 +36,20 @@ const useStyles = makeStyles((theme) => ({
left: 0,
top: 0,
margin: theme.spacing(1.5),
- width: theme.dimensions.drawerWidthDesktop,
+ width: '450px',
bottom: 56,
zIndex: 1301,
transition: 'transform .5s ease',
backgroundColor: 'white',
- [theme.breakpoints.down('md')]: {
+ [theme.breakpoints.down('sm')]: {
width: '100%',
margin: 0,
},
},
sidebarCollapsed: {
- transform: `translateX(-${theme.dimensions.drawerWidthDesktop})`,
+ transform: `translateX(-450px)`,
marginLeft: 0,
- [theme.breakpoints.down('md')]: {
+ [theme.breakpoints.down('sm')]: {
transform: 'translateX(-100vw)',
},
},
@@ -93,25 +95,34 @@ const MainPage = () => {
const classes = useStyles();
const history = useHistory();
const theme = useTheme();
+ const dispatch = useDispatch();
const t = useTranslation();
- const isTablet = useMediaQuery(theme.breakpoints.down('md'));
- const isPhone = useMediaQuery(theme.breakpoints.down('xs'));
+ const isPhone = useMediaQuery(theme.breakpoints.down('sm'));
+ const isWide = useMediaQuery(theme.breakpoints.up('md')/* || theme.breakpoints.up('sm') || theme.breakpoints.up('xs')*/);
- const [deviceName, setDeviceName] = useState('');
- const [collapsed, setCollapsed] = useState(false);
+ const collapsed = useSelector((state) => state.ui.collapsed);
const handleClose = () => {
- setCollapsed(!collapsed);
+ dispatch(uiActions.setCollapsed(!collapsed));
};
- useEffect(() => setCollapsed(isTablet), [isTablet]);
+ const filterTerm = useSelector((state) => state.devices.filterTerm);
+
+ const setFilterTerm = (text) => {
+ dispatch(devicesActions.setFilter(text));
+ }
+
+ const clearFilter = () => {
+ dispatch(devicesActions.clearFilter());
+ }
+
+ useEffect(() => uiActions.setCollapsed(isPhone), [isPhone]);
return (
<div className={classes.root}>
<Map>
- {!isTablet && <MapPadding left={parseInt(theme.dimensions.drawerWidthDesktop, 10)} />}
- <CurrentLocationMap />
+ {isWide && <MapPadding left={parseInt(theme.dimensions.drawerWidthDesktop, 10)} />}
<GeofenceMap />
<AccuracyMap />
<CurrentPositionsMap />
@@ -129,10 +140,10 @@ const MainPage = () => {
<ListIcon />
<div className={classes.sidebarToggleText}>{t('deviceTitle')}</div>
</Button>
- <Paper square elevation={3} className={`${classes.sidebar} ${collapsed && classes.sidebarCollapsed}`}>
+ <Paper square elevation={3} className={`${classes.sidebar} ${(!isWide && collapsed) && classes.sidebarCollapsed}`}>
<Paper className={classes.paper} square elevation={3}>
<Toolbar className={classes.toolbar} disableGutters>
- {isTablet && (
+ {!isWide && (
<IconButton onClick={handleClose}>
<ArrowBackIcon />
</IconButton>
@@ -140,21 +151,20 @@ const MainPage = () => {
<TextField
fullWidth
name="deviceName"
- value={deviceName}
- autoComplete="deviceName"
+ value={filterTerm}
autoFocus
- onChange={(event) => setDeviceName(event.target.value)}
- placeholder="Search Devices"
+ onChange={(event) => setFilterTerm(event.target.value) }
+ placeholder={t("sharedSearch")}
variant="filled"
/>
+ {filterTerm.length > 0 && (
+ <IconButton onClick={() => clearFilter() }>
+ <CloseIcon />
+ </IconButton>
+ )}
<IconButton onClick={() => history.push('/device')}>
<AddIcon />
</IconButton>
- {!isTablet && (
- <IconButton onClick={handleClose}>
- <CloseIcon />
- </IconButton>
- )}
</Toolbar>
</Paper>
<div className={classes.deviceList}>
diff --git a/modern/src/MainToolbar.js b/modern/src/MainToolbar.js
index 1e4dabe..73fdf51 100644
--- a/modern/src/MainToolbar.js
+++ b/modern/src/MainToolbar.js
@@ -5,9 +5,9 @@ import { useDispatch, useSelector } from 'react-redux';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
-import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
+import CloseIcon from '@material-ui/icons/Close';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
@@ -17,7 +17,6 @@ import MapIcon from '@material-ui/icons/Map';
import PersonIcon from '@material-ui/icons/Person';
import DescriptionIcon from '@material-ui/icons/Description';
import ReplayIcon from '@material-ui/icons/Replay';
-import { sessionActions } from './store';
import * as selectors from './common/selectors';
import { useTranslation } from './LocalizationProvider';
@@ -40,7 +39,6 @@ const useStyles = makeStyles((theme) => ({
const MainToolbar = () => {
const classes = useStyles();
const history = useHistory();
- const dispatch = useDispatch();
const t = useTranslation();
const userId = useSelector(selectors.getUserId);
@@ -50,12 +48,8 @@ const MainToolbar = () => {
const openDrawer = () => { setDrawer(true); };
const closeDrawer = () => { setDrawer(false); };
- const handleLogout = async () => {
- const response = await fetch('/api/session', { method: 'DELETE' });
- if (response.ok) {
- dispatch(sessionActions.updateUser(null));
- history.push('/login');
- }
+ const handleClose = async () => {
+ history.push ('/');
};
return (
@@ -70,9 +64,14 @@ const MainToolbar = () => {
<MenuIcon />
</IconButton>
<Typography variant="h6" color="inherit" className={classes.flex}>
- Traccar
+ ETBSA
</Typography>
- <Button color="inherit" onClick={handleLogout}>{t('loginLogout')}</Button>
+ <IconButton
+ color="inherit"
+ onClick={handleClose}
+ >
+ <CloseIcon />
+ </IconButton>
</Toolbar>
</AppBar>
<Drawer open={drawer} onClose={closeDrawer}>
diff --git a/modern/src/StartPage.js b/modern/src/StartPage.js
index 1e204c2..91b93c7 100644
--- a/modern/src/StartPage.js
+++ b/modern/src/StartPage.js
@@ -18,7 +18,7 @@ const useStyles = makeStyles((theme) => ({
[theme.breakpoints.down('md')]: {
width: theme.dimensions.sidebarWidthTablet,
},
- [theme.breakpoints.down('xs')]: {
+ [theme.breakpoints.down('xl')]: {
width: '0px',
},
},
@@ -29,7 +29,7 @@ const useStyles = makeStyles((theme) => ({
alignItems: 'center',
flex: 1,
boxShadow: '-2px 0px 16px rgba(0, 0, 0, 0.25)',
- [theme.breakpoints.up('lg')]: {
+ [theme.breakpoints.up('xl')]: {
padding: theme.spacing(0, 25, 0, 0),
},
},
@@ -43,6 +43,7 @@ const useStyles = makeStyles((theme) => ({
bottom: theme.spacing(1),
right: theme.spacing(1.5),
fontSize: 'x-small',
+ display: 'none',
},
}));
@@ -54,7 +55,7 @@ const StartPage = ({ children }) => {
<>
<main className={classes.root}>
<div className={classes.sidebar}>
- {!useMediaQuery(theme.breakpoints.down('md'))
+ {!useMediaQuery(theme.breakpoints.down('xl'))
&& (
<svg height="64" width="240">
<use xlinkHref="/logo.svg#img" />
diff --git a/modern/src/common/deviceCategories.js b/modern/src/common/deviceCategories.js
index f5d749a..69e4eb5 100644
--- a/modern/src/common/deviceCategories.js
+++ b/modern/src/common/deviceCategories.js
@@ -1,6 +1,7 @@
export default [
'default',
'animal',
+ 'backhoe',
'bicycle',
'boat',
'bus',
diff --git a/modern/src/common/formatter.js b/modern/src/common/formatter.js
index 0ff4e74..f8a55da 100644
--- a/modern/src/common/formatter.js
+++ b/modern/src/common/formatter.js
@@ -4,9 +4,12 @@ export const formatBoolean = (value, t) => (value ? t('sharedYes') : t('sharedNo
export const formatNumber = (value, precision = 1) => Number(value.toFixed(precision));
-export const formatDate = (value, format = 'YYYY-MM-DD HH:mm') => moment(value).format(format);
+export const formatDate = (value, locale) => new Intl.DateTimeFormat(locale, {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+}).format(Date.parse(value));
-export const formatPosition = (value, key, t) => {
+export const formatPosition = (value, key, t, locale = 'es') => {
if (value != null && typeof value === 'object') {
value = value[key];
}
@@ -15,7 +18,7 @@ export const formatPosition = (value, key, t) => {
case 'deviceTime':
case 'serverTime':
case 'eventTime':
- return moment(value).format('LLL');
+ return formatDate(value, locale);
case 'latitude':
case 'longitude':
return value.toFixed(5);
@@ -47,15 +50,7 @@ export const formatDistance = (value, unit, t) => {
};
export const formatSpeed = (value, unit, t) => {
- switch (unit) {
- case 'kmh':
- return `${(value * 1.852).toFixed(2)} ${t('sharedKmh')}`;
- case 'mph':
- return `${(value * 1.15078).toFixed(2)} ${t('sharedMph')}`;
- case 'kn':
- default:
- return `${(value * 1).toFixed(2)} ${t('sharedKn')}`;
- }
+ return `${(value * 1).toFixed(0)} ${t('sharedKmh')}`;
};
export const formatVolume = (value, unit, t) => {
@@ -70,7 +65,10 @@ export const formatVolume = (value, unit, t) => {
}
};
-export const formatHours = (value) => moment.duration(value).humanize();
+export const formatHours = (value, t) => {
+ moment.locale('es');
+ return `${moment.duration(value).asHours().toFixed(0)} ${t('sharedHours')}`;
+}
export const formatCoordinate = (key, value, unit) => {
let hemisphere;
diff --git a/modern/src/common/selectors.js b/modern/src/common/selectors.js
index 0c4c02e..ad42c30 100644
--- a/modern/src/common/selectors.js
+++ b/modern/src/common/selectors.js
@@ -4,4 +4,8 @@ export const getUserId = (state) => state.session.user?.id;
export const getDevices = (state) => Object.values(state.devices.items);
+export const getFilteredDevices = (state) => Object.values(state.devices.filteredItems);
+
+export const getFilterTerm = (state) => state.devices.filterTerm;
+
export const getPosition = (id) => (state) => state.positions.items[id];
diff --git a/modern/src/components/BottomMenu.js b/modern/src/components/BottomMenu.js
index a1e9625..59ad96e 100644
--- a/modern/src/components/BottomMenu.js
+++ b/modern/src/components/BottomMenu.js
@@ -10,7 +10,7 @@ import ShuffleIcon from '@material-ui/icons/Shuffle';
import MapIcon from '@material-ui/icons/Map';
import LogoutIcon from '@material-ui/icons/ExitToApp';
-import { sessionActions } from '../store';
+import { sessionActions, uiActions } from '../store';
import { useTranslation } from '../LocalizationProvider';
const useStyles = makeStyles((theme) => ({
@@ -20,10 +20,10 @@ const useStyles = makeStyles((theme) => ({
width: '100%',
position: 'fixed',
zIndex: 1301,
- [theme.breakpoints.up('lg')]: {
+ [theme.breakpoints.up('md')]: {
left: theme.spacing(1.5),
bottom: theme.spacing(1.5),
- width: theme.dimensions.drawerWidthDesktop,
+ width: '450px',
},
},
}));
@@ -37,6 +37,9 @@ const BottomMenu = () => {
const handleSelection = async (_, value) => {
switch (value) {
+ case 0:
+ dispatch(uiActions.setCollapsed(true));
+ break;
case 1:
history.push('/reports/route');
break;
@@ -47,6 +50,7 @@ const BottomMenu = () => {
await fetch('/api/session', { method: 'DELETE' });
history.push('/login');
dispatch(sessionActions.updateUser(null));
+ window.location.reload();
break;
default:
break;
diff --git a/modern/src/components/registration/LoginForm.js b/modern/src/components/registration/LoginForm.js
index bbb5d5f..9c1fbb4 100644
--- a/modern/src/components/registration/LoginForm.js
+++ b/modern/src/components/registration/LoginForm.js
@@ -63,14 +63,9 @@ const LoginForm = () => {
return (
<StartPage>
<Grid container direction="column" spacing={2}>
- {useMediaQuery(theme.breakpoints.down('md'))
- && (
- <Grid item className={classes.logoContainer}>
- <svg height="64" width="240">
- <use xlinkHref="/logo.svg#img" />
- </svg>
- </Grid>
- )}
+ <Grid item className={classes.logoContainer}>
+ <img src="/logo.svg" height="64" width="240" />
+ </Grid>
<Grid item>
<TextField
required
diff --git a/modern/src/index.js b/modern/src/index.js
index 155bf62..32cf990 100644
--- a/modern/src/index.js
+++ b/modern/src/index.js
@@ -16,4 +16,4 @@ ReactDOM.render((
</Provider>
), document.getElementById('root'));
-serviceWorker.register();
+serviceWorker.unregister();
diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js
index dcf5a92..8c9bf51 100644
--- a/modern/src/map/Map.js
+++ b/modern/src/map/Map.js
@@ -8,10 +8,9 @@ import { SwitcherControl } from './switcher/switcher';
import deviceCategories from '../common/deviceCategories';
import { prepareIcon, loadImage } from './mapUtil';
import {
- styleCarto, styleLocationIq, styleMapbox, styleMapTiler, styleOsm,
+ styleLocationIq, styleCarto, styleOsm, styleGmapsStreets, styleGmapsSatellite, styleGmapsHybrid
} from './mapStyles';
import { useAttributePreference } from '../common/preferences';
-import palette from '../theme/palette';
import { useTranslation } from '../LocalizationProvider';
const element = document.createElement('div');
@@ -20,6 +19,8 @@ element.style.height = '100%';
export const map = new maplibregl.Map({
container: element,
+ center: [-100.360, 23.191],
+ zoom: 5
});
let ready = false;
@@ -43,18 +44,16 @@ const initMap = async () => {
if (ready) return;
if (!map.hasImage('background')) {
const background = await loadImage('images/background.svg');
- map.addImage('background', await prepareIcon(background), {
+ map.addImage('background', prepareIcon(background), {
pixelRatio: window.devicePixelRatio,
});
await Promise.all(deviceCategories.map(async (category) => {
const results = [];
- ['green', 'red', 'gray'].forEach((color) => {
- results.push(loadImage(`images/icon/${category}.svg`).then((icon) => {
- map.addImage(`${category}-${color}`, prepareIcon(background, icon, palette.common[color]), {
- pixelRatio: window.devicePixelRatio,
- });
- }));
- });
+ results.push(loadImage(`images/icon/${category.toLowerCase()}.png`).then((icon) => {
+ map.addImage(`${category.toLowerCase()}-map`, prepareIcon(background, icon, null), {
+ pixelRatio: window.devicePixelRatio,
+ });
+ }));
await Promise.all(results);
}));
}
@@ -62,7 +61,7 @@ const initMap = async () => {
};
map.addControl(new maplibregl.NavigationControl({
- showCompass: false,
+ showCompass: false
}));
const switcher = new SwitcherControl(
@@ -70,7 +69,7 @@ const switcher = new SwitcherControl(
() => {
const waiting = () => {
if (!map.loaded()) {
- setTimeout(waiting, 100);
+ setTimeout(waiting, 2000);
} else {
initMap();
}
@@ -98,17 +97,13 @@ const Map = ({ children }) => {
useEffect(() => {
switcher.updateStyles([
{ id: 'locationIqStreets', title: t('mapLocationIqStreets'), uri: styleLocationIq('streets', locationIqKey) },
- { id: 'locationIqEarth', title: t('mapLocationIqEarth'), uri: styleLocationIq('earth', locationIqKey) },
- { id: 'locationIqHybrid', title: t('mapLocationIqHybrid'), uri: styleLocationIq('hybrid', locationIqKey) },
{ id: 'osm', title: t('mapOsm'), uri: styleOsm() },
{ id: 'carto', title: t('mapCarto'), uri: styleCarto() },
- { id: 'mapboxStreets', title: t('mapMapboxStreets'), uri: styleMapbox('streets-v11') },
- { id: 'mapboxOutdoors', title: t('mapMapboxOutdoors'), uri: styleMapbox('outdoors-v11') },
- { id: 'mapboxSatellite', title: t('mapMapboxSatellite'), uri: styleMapbox('satellite-v9') },
- { id: 'mapTilerBasic', title: t('mapMapTilerBasic'), uri: styleMapTiler('basic', mapTilerKey) },
- { id: 'mapTilerHybrid', title: t('mapMapTilerHybrid'), uri: styleMapTiler('hybrid', mapTilerKey) },
- ], 'locationIqStreets');
- }, [mapTilerKey]);
+ { id: 'gmapsStreets', title: t('mapGmapsStreets'), uri: styleGmapsStreets() },
+ { id: 'gmapsSatellite', title: t('mapGmapsSatellite'), uri: styleGmapsSatellite() },
+ { id: 'gmapsHybrid', title: t('mapGmapsHybrid'), uri: styleGmapsHybrid() },
+ ], 'gmapsStreets');
+ }, [locationIqKey]);
useEffect(() => {
const listener = (ready) => setMapReady(ready);
diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js
index 8d10053..b702400 100644
--- a/modern/src/map/PositionsMap.js
+++ b/modern/src/map/PositionsMap.js
@@ -18,19 +18,24 @@ const PositionsMap = ({ positions }) => {
const devices = useSelector((state) => state.devices.items);
const deviceColor = (device) => {
- switch (device.status) {
- case 'online':
+ const position = positions[device.id];
+ if (position) {
+ if (position.attributes.ignition) {
return 'green';
- case 'offline':
- return 'red';
- default:
+ } else if (position.attributes.ignition === undefined) {
return 'gray';
+ } else {
+ return 'red';
+ }
+ } else {
+ return 'gray';
}
};
const createFeature = (devices, position) => {
const device = devices[position.deviceId];
return {
+ position: position,
deviceId: position.deviceId,
name: device.name,
category: device.category || 'default',
@@ -48,15 +53,20 @@ const PositionsMap = ({ positions }) => {
coordinates[0] += event.lngLat.lng > coordinates[0] ? 360 : -360;
}
+ console.log(event);
+
+ const position = JSON.parse(feature.properties.position);
const placeholder = document.createElement('div');
ReactDOM.render(
<Provider store={store}>
<ThemeProvider theme={theme}>
- <StatusView
- deviceId={feature.properties.deviceId}
- onShowDetails={(positionId) => history.push(`/position/${positionId}`)}
- onShowHistory={() => history.push('/replay')}
- onEditClick={(deviceId) => history.push(`/device/${deviceId}`)}
+ <StatusView
+ position={position}
+ deviceId={feature.properties.deviceId}
+ onShowDetails={(positionId) => history.push(`/position/${positionId}`)}
+ onShowHistory={(deviceId) => history.push(`/replay/${deviceId}`)}
+ onEditClick={(deviceId) => history.push(`/device/${deviceId}`)}
+ onCommandsClick={(deviceId) => history.push(`/device/${deviceId}/commands`) }
/>
</ThemeProvider>
</Provider>,
@@ -104,18 +114,19 @@ const PositionsMap = ({ positions }) => {
source: id,
filter: ['!', ['has', 'point_count']],
layout: {
- 'icon-image': '{category}-{color}',
+ 'icon-image': ['concat', ['downcase', ['get', 'category']], '-map'],
'icon-allow-overlap': true,
'text-field': '{name}',
'text-allow-overlap': true,
'text-anchor': 'bottom',
'text-offset': [0, -2],
- 'text-font': ['Roboto Regular'],
+ 'text-font': ['Roboto Bold'],
'text-size': 12,
},
paint: {
+ 'text-color': 'black',
'text-halo-color': 'white',
- 'text-halo-width': 1,
+ 'text-halo-width': 2,
},
});
map.addLayer({
diff --git a/modern/src/map/SelectedDeviceMap.js b/modern/src/map/SelectedDeviceMap.js
index 6384717..d05394b 100644
--- a/modern/src/map/SelectedDeviceMap.js
+++ b/modern/src/map/SelectedDeviceMap.js
@@ -16,10 +16,10 @@ const SelectedDeviceMap = () => {
useEffect(() => {
if (mapCenter) {
- map.easeTo({ center: mapCenter.position });
+ map.easeTo({ center: mapCenter.position, zoom:18 });
}
}, [mapCenter]);
-
+
return null;
};
diff --git a/modern/src/map/StatusView.js b/modern/src/map/StatusView.js
index 5526e14..b892e77 100644
--- a/modern/src/map/StatusView.js
+++ b/modern/src/map/StatusView.js
@@ -4,17 +4,18 @@ import {
} from '@material-ui/core';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
-import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import ListItemText from '@material-ui/core/ListItemText';
import { useSelector } from 'react-redux';
-import ReplayIcon from '@material-ui/icons/Replay';
-import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import EditIcon from '@material-ui/icons/Edit';
+import SendIcon from '@material-ui/icons/Send';
import DeleteIcon from '@material-ui/icons/Delete';
+import LinkIcon from '@material-ui/icons/Link';
+import InfoIcon from '@material-ui/icons/Info';
+import PlayCircleFilledIcon from '@material-ui/icons/PlayCircleFilled';
import {
- formatPosition, getStatusColor, getBatteryStatus, formatDistance, formatSpeed,
+ formatSpeed, formatHours, formatPosition
} from '../common/formatter';
import { useAttributePreference } from '../common/preferences';
import RemoveDialog from '../RemoveDialog';
@@ -27,19 +28,23 @@ const useStyles = makeStyles((theme) => ({
},
...theme.palette.colors,
listItemContainer: {
- maxWidth: '240px',
+ maxWidth: '300px',
},
+ listItemRoot: {
+ paddingTop: '0px',
+ paddingBottom: '0px',
+ }
}));
const StatusView = ({
- deviceId, onShowDetails, onShowHistory, onEditClick,
+ position, deviceId, onShowDetails, onShowHistory, onEditClick, onCommandsClick,
}) => {
const classes = useStyles();
const t = useTranslation();
const [removeDialogShown, setRemoveDialogShown] = useState(false);
+ const session = useSelector((state) => state.session);
const device = useSelector((state) => state.devices.items[deviceId]);
- const position = useSelector(getPosition(deviceId));
const distanceUnit = useAttributePreference('distanceUnit');
const speedUnit = useAttributePreference('speedUnit');
@@ -54,6 +59,11 @@ const StatusView = ({
onEditClick(deviceId);
};
+ const handleCommandsClick = (e) => {
+ e.preventDefault();
+ onCommandsClick(deviceId);
+ }
+
const handleRemove = () => {
setRemoveDialogShown(true);
};
@@ -62,72 +72,71 @@ const StatusView = ({
setRemoveDialogShown(false);
};
+ const handleGotoLink = () => {
+ const url = `https://maps.google.com/maps?q=${position.latitude},${position.longitude}&z=18`;
+ window.open(url, "_blank");
+ }
+
return (
<>
<Paper className={classes.paper} elevation={0} square>
<Grid container direction="column">
<Grid item>
<List>
- <ListItem classes={{ container: classes.listItemContainer }}>
- <ListItemText primary={t('deviceStatus')} />
- <ListItemSecondaryAction>
- <span className={classes[getStatusColor(device.status)]}>{device.status}</span>
- </ListItemSecondaryAction>
- </ListItem>
- <ListItem classes={{ container: classes.listItemContainer }}>
- <ListItemText primary={t('positionSpeed')} />
- <ListItemSecondaryAction>
- {formatSpeed(position.speed, speedUnit, t)}
- </ListItemSecondaryAction>
+ <ListItem classes={{ container: classes.listItemContainer, root: classes.listItemRoot }} >
+ <ListItemText primary={t('positionDatetime')} secondary={formatPosition(position, 'fixTime', t)} />
</ListItem>
- {position.attributes.batteryLevel && (
- <ListItem classes={{ container: classes.listItemContainer }}>
- <ListItemText primary={t('positionBattery')} />
- <ListItemSecondaryAction>
- <span className={classes[getBatteryStatus(position.attributes.batteryLevel)]}>
- {formatPosition(position.attributes.batteryLevel, 'batteryLevel', t)}
- </span>
- </ListItemSecondaryAction>
- </ListItem>
- )}
- <ListItem classes={{ container: classes.listItemContainer }}>
- <ListItemText primary={t('positionDistance')} />
- <ListItemSecondaryAction>
- {formatDistance(position.attributes.totalDistance, distanceUnit, t)}
- </ListItemSecondaryAction>
- </ListItem>
- <ListItem classes={{ container: classes.listItemContainer }}>
- <ListItemText primary={t('positionCourse')} />
- <ListItemSecondaryAction>
- {formatPosition(position.course, 'course', t)}
- </ListItemSecondaryAction>
+ <ListItem classes={{ container: classes.listItemContainer, root: classes.listItemRoot }}>
+ <ListItemText primary={t('positionSpeed')} secondary={formatSpeed(position.speed, speedUnit, t)} />
</ListItem>
+ {device.category
+ && (device.category.toLowerCase() === 'backhoe' || device.category.toLowerCase() === 'tractor')
+ && position.attributes.hours
+ && (
+ <ListItem classes={{ container: classes.listItemContainer, root: classes.listItemRoot }}>
+ <ListItemText primary={t('positionHours')} secondary={formatHours(position.attributes.hours, t)} />
+ </ListItem>)}
+ {position.address && (
+ <ListItem classes={{ container: classes.listItemContainer, root: classes.listItemRoot }}>
+ <ListItemText primary={t('positionAddress')} secondary={position.address} />
+ </ListItem>)}
</List>
</Grid>
<Grid item container>
<Grid item>
- <Button color="secondary" onClick={handleClick}>More Info</Button>
- </Grid>
- <Grid item>
- <IconButton onClick={onShowHistory}>
- <ReplayIcon />
+ <IconButton onClick={handleClick}>
+ <InfoIcon />
</IconButton>
</Grid>
<Grid item>
- <IconButton>
- <ExitToAppIcon />
+ <IconButton onClick={() => onShowHistory(deviceId)}>
+ <PlayCircleFilledIcon />
</IconButton>
</Grid>
<Grid item>
- <IconButton onClick={handleEditClick}>
- <EditIcon />
+ <IconButton onClick={handleGotoLink}>
+ <LinkIcon />
</IconButton>
</Grid>
<Grid item>
- <IconButton onClick={handleRemove} className={classes.red}>
- <DeleteIcon />
+ <IconButton onClick={handleCommandsClick}>
+ <SendIcon />
</IconButton>
</Grid>
+ {!session.server.deviceReadonly && (
+ <>
+ <Grid item>
+ <IconButton onClick={handleEditClick}>
+ <EditIcon />
+ </IconButton>
+ </Grid>
+ <Grid item>
+ <IconButton onClick={handleRemove} className={classes.red}>
+ <DeleteIcon />
+ </IconButton>
+ </Grid>
+ </>
+ )}
</Grid>
</Grid>
</Paper>
diff --git a/modern/src/map/mapStyles.js b/modern/src/map/mapStyles.js
index 86813a1..a6e84fd 100644
--- a/modern/src/map/mapStyles.js
+++ b/modern/src/map/mapStyles.js
@@ -53,3 +53,11 @@ export const styleMapbox = (style) => `mapbox://styles/mapbox/${style}`;
export const styleMapTiler = (style, key) => `https://api.maptiler.com/maps/${style}/style.json?key=${key}`;
export const styleLocationIq = (style, key) => `https://tiles.locationiq.com/v3/${style}/vector.json?key=${key}`;
+
+// Google Maps
+
+export const styleGmapsStreets = () => styleCustom('https://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}&s=Ga', '');
+
+export const styleGmapsSatellite = () => styleCustom('https://mt0.google.com/vt/lyrs=s&hl=en&x={x}&y={y}&z={z}&s=Ga', '');
+
+export const styleGmapsHybrid = () => styleCustom('https://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}&s=Ga', '');
diff --git a/modern/src/map/mapUtil.js b/modern/src/map/mapUtil.js
index 2aa86c6..e93c146 100644
--- a/modern/src/map/mapUtil.js
+++ b/modern/src/map/mapUtil.js
@@ -39,14 +39,10 @@ export const prepareIcon = (background, icon, color) => {
context.drawImage(background, 0, 0, canvas.width, canvas.height);
if (icon) {
- const iconRatio = 0.5;
+ const iconRatio = 0.7;
const imageWidth = canvas.width * iconRatio;
const imageHeight = canvas.height * iconRatio;
- if (navigator.userAgent.indexOf('Firefox') > 0) {
- context.drawImage(icon, (canvas.width - imageWidth) / 2, (canvas.height - imageHeight) / 2, imageWidth, imageHeight);
- } else {
- context.drawImage(canvasTintImage(icon, color), (canvas.width - imageWidth) / 2, (canvas.height - imageHeight) / 2, imageWidth, imageHeight);
- }
+ context.drawImage(icon, (canvas.width - imageWidth) / 2, (canvas.height - imageHeight) / 2, imageWidth, imageHeight);
}
return context.getImageData(0, 0, canvas.width, canvas.height);
diff --git a/modern/src/reports/ReplayPage.js b/modern/src/reports/ReplayPage.js
index bb32daa..fa94a06 100644
--- a/modern/src/reports/ReplayPage.js
+++ b/modern/src/reports/ReplayPage.js
@@ -1,15 +1,21 @@
import React, { useState } from 'react';
+import { useParams } from 'react-router-dom';
import {
- Accordion, AccordionDetails, AccordionSummary, Container, makeStyles, Paper, Slider, Tooltip, Typography,
+ Accordion, AccordionDetails, AccordionSummary, Container, makeStyles, Paper, Slider, Tooltip, Typography, IconButton, Box
} from '@material-ui/core';
+
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import SkipNextIcon from '@material-ui/icons/SkipNext';
+import SkipPreviousIcon from '@material-ui/icons/SkipPrevious';
+
import MainToolbar from '../MainToolbar';
import Map from '../map/Map';
import ReplayPathMap from '../map/ReplayPathMap';
import PositionsMap from '../map/PositionsMap';
-import { formatPosition } from '../common/formatter';
+import { formatPosition, formatSpeed } from '../common/formatter';
import ReportFilter from './ReportFilter';
import { useTranslation } from '../LocalizationProvider';
+import { useAttributePreference } from '../common/preferences';
const useStyles = makeStyles((theme) => ({
root: {
@@ -41,8 +47,11 @@ const TimeLabel = ({ children, open, value }) => (
const ReplayPage = () => {
const classes = useStyles();
+ const { id } = useParams();
const t = useTranslation();
+ const speedUnit = useAttributePreference('speedUnit');
+
const [expanded, setExpanded] = useState(true);
const [positions, setPositions] = useState([]);
const [index, setIndex] = useState(0);
@@ -68,18 +77,39 @@ const ReplayPage = () => {
<Container maxWidth="sm" className={classes.controlPanel}>
{!!positions.length
&& (
- <Paper className={classes.controlContent}>
- <Slider
- max={positions.length - 1}
- step={null}
- marks={positions.map((_, index) => ({ value: index }))}
- value={index}
- onChange={(_, index) => setIndex(index)}
- valueLabelDisplay="auto"
- valueLabelFormat={(i) => (i < positions.length ? formatPosition(positions[i], 'fixTime', t) : '')}
- ValueLabelComponent={TimeLabel}
- />
- </Paper>
+ <Paper className={classes.controlContent}>
+ <Box maxWidth="sm" display="flex">
+ {/* Previous position button */}
+ <IconButton color="primary" disabled={index === 0} onClick={() => setIndex(index - 1)}>
+ <SkipPreviousIcon />
+ </IconButton>
+
+ <Box flexGrow={1} textAlign="center">
+ {/* Date time */}
+ <Typography variant="button">{formatPosition(positions[index], 'fixTime', t)}</Typography>
+ {/* Speed */}
+ {positions[index].speed != undefined &&
+ <>
+ <br/><Typography variant="secondary">{formatSpeed(positions[index].speed, speedUnit, t)}</Typography>
+ </>}
+ </Box>
+ {/* Speed */}
+ {/* Next position button*/}
+ <IconButton color="primary" disabled={index === positions.length - 1} onClick={() => setIndex(index + 1)}>
+ <SkipNextIcon />
+ </IconButton>
+ </Box>
+ <Slider
+ max={positions.length - 1}
+ step={null}
+ marks={positions.map((_, index) => ({ value: index }))}
+ value={index}
+ onChange={(_, index) => setIndex(index)}
+ valueLabelDisplay="auto"
+ valueLabelFormat={(i) => (i < positions.length ? formatPosition(positions[i], 'fixTime', t) : '')}
+ ValueLabelComponent={TimeLabel}
+ />
+ </Paper>
)}
<div>
<Accordion expanded={expanded} onChange={() => setExpanded(!expanded)}>
@@ -89,7 +119,7 @@ const ReplayPage = () => {
</Typography>
</AccordionSummary>
<AccordionDetails className={classes.configForm}>
- <ReportFilter handleSubmit={handleSubmit} showOnly />
+ <ReportFilter handleSubmit={handleSubmit} showOnly defaultSelected={id} />
</AccordionDetails>
</Accordion>
</div>
diff --git a/modern/src/reports/ReportFilter.js b/modern/src/reports/ReportFilter.js
index 23c7fc0..25b0af7 100644
--- a/modern/src/reports/ReportFilter.js
+++ b/modern/src/reports/ReportFilter.js
@@ -6,11 +6,11 @@ import { useSelector } from 'react-redux';
import moment from 'moment';
import { useTranslation } from '../LocalizationProvider';
-const ReportFilter = ({ children, handleSubmit, showOnly }) => {
+const ReportFilter = ({ children, handleSubmit, showOnly, defaultSelected }) => {
const t = useTranslation();
const devices = useSelector((state) => Object.values(state.devices.items));
- const [deviceId, setDeviceId] = useState();
+ const [deviceId, setDeviceId] = useState(defaultSelected);
const [period, setPeriod] = useState('today');
const [from, setFrom] = useState(moment().subtract(1, 'hour'));
const [to, setTo] = useState(moment());
@@ -65,9 +65,11 @@ const ReportFilter = ({ children, handleSubmit, showOnly }) => {
<FormControl variant="filled" fullWidth>
<InputLabel>{t('reportDevice')}</InputLabel>
<Select value={deviceId} onChange={(e) => setDeviceId(e.target.value)}>
- {devices.map((device) => (
- <MenuItem value={device.id}>{device.name}</MenuItem>
- ))}
+ {devices
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((device) => (
+ <MenuItem value={device.id}>{device.name}</MenuItem>
+ ))}
</Select>
</FormControl>
</Grid>
diff --git a/modern/src/setupProxy.js b/modern/src/setupProxy.js
index cd6a2fa..bc0daf0 100644
--- a/modern/src/setupProxy.js
+++ b/modern/src/setupProxy.js
@@ -3,6 +3,6 @@
const proxy = require('http-proxy-middleware');
module.exports = (app) => {
- app.use(proxy('/api/socket', { target: `ws://${process.env.REACT_APP_URL_NAME}`, ws: true }));
- app.use(proxy('/api', { target: `http://${process.env.REACT_APP_URL_NAME}` }));
-};
+ app.use(proxy('/api/socket', { target: `wss://${process.env.REACT_APP_URL_NAME}`, changeOrigin: true, ws: true }));
+ app.use(proxy('/api', { target: `https://${process.env.REACT_APP_URL_NAME}`, changeOrigin: true }));
+}; \ No newline at end of file
diff --git a/modern/src/store/devices.js b/modern/src/store/devices.js
index cca23cb..f63602a 100644
--- a/modern/src/store/devices.js
+++ b/modern/src/store/devices.js
@@ -1,9 +1,11 @@
-import { createSlice } from '@reduxjs/toolkit';
+import { createSlice, current } from '@reduxjs/toolkit';
const { reducer, actions } = createSlice({
name: 'devices',
initialState: {
items: {},
+ filterTerm: '',
+ filteredItems: {},
selectedId: null,
},
reducers: {
@@ -17,6 +19,20 @@ const { reducer, actions } = createSlice({
select(state, action) {
state.selectedId = action.payload.id;
},
+ unselect(state, action) {
+ state.selectedId = null;
+ },
+ setFilter(state, action) {
+ state.filterTerm = action.payload;
+ const items = Object.entries(current(state).items);
+ state.filteredItems = Object.fromEntries(items.filter(([k, v]) => {
+ return v.name.toLowerCase().includes(action.payload.toLowerCase());
+ }));
+ },
+ clearFilter(state, action) {
+ state.filterTerm = '';
+ state.filteredItems = {};
+ },
remove(state, action) {
delete state.items[action.payload];
},
diff --git a/modern/src/store/index.js b/modern/src/store/index.js
index 6e2bb20..ebd3f43 100644
--- a/modern/src/store/index.js
+++ b/modern/src/store/index.js
@@ -7,6 +7,7 @@ import { geofencesReducer as geofences } from './geofences';
import { groupsReducer as groups } from './groups';
import { driversReducer as drivers } from './drivers';
import { maintenancesReducer as maintenances } from './maintenances';
+import { uiReducer as ui } from './ui';
const reducer = combineReducers({
session,
@@ -16,6 +17,7 @@ const reducer = combineReducers({
groups,
drivers,
maintenances,
+ ui
});
export { sessionActions } from './session';
@@ -25,5 +27,6 @@ export { geofencesActions } from './geofences';
export { groupsActions } from './groups';
export { driversActions } from './drivers';
export { maintenancesActions } from './maintenances';
+export { uiActions } from './ui';
export default configureStore({ reducer });
diff --git a/modern/src/store/ui.js b/modern/src/store/ui.js
new file mode 100644
index 0000000..0783492
--- /dev/null
+++ b/modern/src/store/ui.js
@@ -0,0 +1,16 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const { reducer, actions } = createSlice({
+ name: 'ui',
+ initialState: {
+ collapsed: true
+ },
+ reducers: {
+ setCollapsed(state, action) {
+ state.collapsed = action.payload;
+ },
+ },
+});
+
+export { actions as uiActions };
+export { reducer as uiReducer };
diff --git a/modern/src/theme/index.js b/modern/src/theme/index.js
index dc0a35b..02865c2 100644
--- a/modern/src/theme/index.js
+++ b/modern/src/theme/index.js
@@ -1,9 +1,9 @@
-import { createMuiTheme } from '@material-ui/core/styles';
+import { createTheme } from '@material-ui/core/styles';
import palette from './palette';
import overrides from './overrides';
import dimensions from './dimensions';
-const theme = createMuiTheme({
+const theme = createTheme({
palette,
overrides,
dimensions,