import React, { useState } from 'react'; import dayjs from 'dayjs'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { Accordion, AccordionSummary, AccordionDetails, Typography, Container, FormControl, InputLabel, Select, MenuItem, Checkbox, FormControlLabel, FormGroup, InputAdornment, IconButton, OutlinedInput, Autocomplete, TextField, createFilterOptions, Button, } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import CachedIcon from '@mui/icons-material/Cached'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider'; import PageLayout from '../common/components/PageLayout'; import SettingsMenu from './components/SettingsMenu'; import usePositionAttributes from '../common/attributes/usePositionAttributes'; import { prefixString, unprefixString } from '../common/util/stringUtils'; import SelectField from '../common/components/SelectField'; import useMapStyles from '../map/core/useMapStyles'; import useMapOverlays from '../map/overlay/useMapOverlays'; import { useCatch } from '../reactHelper'; import { sessionActions } from '../store'; import { useRestriction } from '../common/util/permissions'; const deviceFields = [ { id: 'name', name: 'sharedName' }, { id: 'uniqueId', name: 'deviceIdentifier' }, { id: 'phone', name: 'sharedPhone' }, { id: 'model', name: 'deviceModel' }, { id: 'contact', name: 'deviceContact' }, ]; const useStyles = makeStyles((theme) => ({ container: { marginTop: theme.spacing(2), }, buttons: { marginTop: theme.spacing(2), marginBottom: theme.spacing(2), display: 'flex', justifyContent: 'space-evenly', '& > *': { flexBasis: '33%', }, }, details: { display: 'flex', flexDirection: 'column', gap: theme.spacing(2), paddingBottom: theme.spacing(3), }, tokenActions: { display: 'flex', flexDirection: 'column', }, })); const PreferencesPage = () => { const classes = useStyles(); const dispatch = useDispatch(); const navigate = useNavigate(); const t = useTranslation(); const readonly = useRestriction('readonly'); const user = useSelector((state) => state.session.user); const [attributes, setAttributes] = useState(user.attributes); const versionApp = import.meta.env.VITE_APP_VERSION.slice(0, -2); const versionServer = useSelector((state) => state.session.server.version); const socket = useSelector((state) => state.session.socket); const [token, setToken] = useState(null); const [tokenExpiration, setTokenExpiration] = useState(dayjs().add(1, 'week').locale('en').format('YYYY-MM-DD')); const mapStyles = useMapStyles(); const mapOverlays = useMapOverlays(); const positionAttributes = usePositionAttributes(t); const filter = createFilterOptions(); const generateToken = useCatch(async () => { const expiration = dayjs(tokenExpiration, 'YYYY-MM-DD').toISOString(); const response = await fetch('/api/session/token', { method: 'POST', body: new URLSearchParams(`expiration=${expiration}`), }); if (response.ok) { setToken(await response.text()); } else { throw Error(await response.text()); } }); const alarms = useTranslationKeys((it) => it.startsWith('alarm')).map((it) => ({ key: unprefixString('alarm', it), name: t(it), })); const handleSave = useCatch(async () => { const response = await fetch(`/api/users/${user.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...user, attributes }), }); if (response.ok) { dispatch(sessionActions.updateUser(await response.json())); navigate(-1); } else { throw Error(await response.text()); } }); return ( } breadcrumbs={['settingsTitle', 'sharedPreferences']}> {!readonly && ( <> }> {t('mapTitle')} {t('mapActive')} { const clicked = mapStyles.find((s) => s.id === child.props.value); if (clicked.available) { setAttributes({ ...attributes, activeMapStyles: e.target.value.join(',') }); } else if (clicked.id !== 'custom') { const query = new URLSearchParams({ attribute: clicked.attribute }); navigate(`/settings/user/${user.id}?${query.toString()}`); } }} multiple > {mapStyles.map((style) => ( {style.title} ))} {t('mapOverlay')} { const clicked = mapOverlays.find((o) => o.id === e.target.value); if (!clicked || clicked.available) { setAttributes({ ...attributes, selectedMapOverlay: e.target.value }); } else if (clicked.id !== 'custom') { const query = new URLSearchParams({ attribute: clicked.attribute }); navigate(`/settings/user/${user.id}?${query.toString()}`); } }} > {'\u00a0'} {mapOverlays.map((overlay) => ( {overlay.title} ))} (positionAttributes[option]?.name || option)} value={attributes.positionItems?.split(',') || ['speed', 'address', 'totalDistance', 'course']} onChange={(_, option) => { setAttributes({ ...attributes, positionItems: option.join(',') }); }} filterOptions={(options, params) => { const filtered = filter(options, params); if (params.inputValue && !filtered.includes(params.inputValue)) { filtered.push(params.inputValue); } return filtered; }} renderInput={(params) => ( )} /> {t('mapLiveRoutes')} setAttributes({ ...attributes, mapLiveRoutes: e.target.value })} > {t('sharedDisabled')} {t('deviceSelected')} {t('notificationAlways')} {t('mapDirection')} setAttributes({ ...attributes, mapDirection: e.target.value })} > {t('sharedDisabled')} {t('deviceSelected')} {t('notificationAlways')} setAttributes({ ...attributes, mapGeofences: e.target.checked })} /> )} label={t('attributeShowGeofences')} /> setAttributes({ ...attributes, mapFollow: e.target.checked })} /> )} label={t('deviceFollow')} /> setAttributes({ ...attributes, mapCluster: e.target.checked })} /> )} label={t('mapClustering')} /> setAttributes({ ...attributes, mapOnSelect: e.target.checked })} /> )} label={t('mapOnSelect')} /> }> {t('deviceTitle')} setAttributes({ ...attributes, devicePrimary: e.target.value })} data={deviceFields} titleGetter={(it) => t(it.name)} label={t('devicePrimaryInfo')} /> setAttributes({ ...attributes, deviceSecondary: e.target.value })} data={deviceFields} titleGetter={(it) => t(it.name)} label={t('deviceSecondaryInfo')} /> }> {t('sharedSound')} setAttributes({ ...attributes, soundEvents: e.target.value.join(',') })} endpoint="/api/notifications/types" keyGetter={(it) => it.type} titleGetter={(it) => t(prefixString('event', it.type))} label={t('eventsSoundEvents')} /> setAttributes({ ...attributes, soundAlarms: e.target.value.join(',') })} data={alarms} keyGetter={(it) => it.key} label={t('eventsSoundAlarms')} /> > )} }> {t('userToken')} { setTokenExpiration(e.target.value); setToken(null); }} /> navigator.clipboard.writeText(token)} disabled={!token}> )} /> {!readonly && ( <> }> {t('sharedInfoTitle')} navigate(-1)} > {t('sharedCancel')} {t('sharedSave')} > )} ); }; export default PreferencesPage;