diff options
Diffstat (limited to 'modern/src')
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, |