diff options
Diffstat (limited to 'modern/src/settings')
38 files changed, 0 insertions, 4852 deletions
diff --git a/modern/src/settings/AccumulatorsPage.jsx b/modern/src/settings/AccumulatorsPage.jsx deleted file mode 100644 index 1c9b6e65..00000000 --- a/modern/src/settings/AccumulatorsPage.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, - TextField, - Button, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatch } from '../reactHelper'; -import { useAttributePreference } from '../common/util/preferences'; -import { distanceFromMeters, distanceToMeters, distanceUnitString } from '../common/util/converter'; -import useSettingsStyles from './common/useSettingsStyles'; - -const AccumulatorsPage = () => { - const navigate = useNavigate(); - const classes = useSettingsStyles(); - const t = useTranslation(); - - const distanceUnit = useAttributePreference('distanceUnit'); - - const { deviceId } = useParams(); - const position = useSelector((state) => state.session.positions[deviceId]); - - const [item, setItem] = useState(); - - useEffect(() => { - if (position) { - setItem({ - deviceId: parseInt(deviceId, 10), - hours: position.attributes.hours || 0, - totalDistance: position.attributes.totalDistance || 0, - }); - } - }, [deviceId, position]); - - const handleSave = useCatch(async () => { - const response = await fetch(`/api/devices/${deviceId}/accumulators`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['sharedDeviceAccumulators']}> - {item && ( - <Container maxWidth="xs" className={classes.container}> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - type="number" - value={item.hours / 3600000} - onChange={(event) => setItem({ ...item, hours: Number(event.target.value) * 3600000 })} - label={t('positionHours')} - /> - <TextField - type="number" - value={distanceFromMeters(item.totalDistance, distanceUnit)} - onChange={(event) => setItem({ ...item, totalDistance: distanceToMeters(Number(event.target.value), distanceUnit) })} - label={`${t('deviceTotalDistance')} (${distanceUnitString(distanceUnit, t)})`} - /> - </AccordionDetails> - </Accordion> - <div className={classes.buttons}> - <Button - type="button" - color="primary" - variant="outlined" - onClick={() => navigate(-1)} - > - {t('sharedCancel')} - </Button> - <Button - type="button" - color="primary" - variant="contained" - onClick={handleSave} - > - {t('sharedSave')} - </Button> - </div> - </Container> - )} - </PageLayout> - ); -}; - -export default AccumulatorsPage; diff --git a/modern/src/settings/AnnouncementPage.jsx b/modern/src/settings/AnnouncementPage.jsx deleted file mode 100644 index 39488f02..00000000 --- a/modern/src/settings/AnnouncementPage.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, - TextField, - Button, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatchCallback } from '../reactHelper'; -import useSettingsStyles from './common/useSettingsStyles'; -import SelectField from '../common/components/SelectField'; -import { prefixString } from '../common/util/stringUtils'; - -const AnnouncementPage = () => { - const navigate = useNavigate(); - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [users, setUsers] = useState([]); - const [notificator, setNotificator] = useState(); - const [message, setMessage] = useState({}); - - const handleSend = useCatchCallback(async () => { - const query = new URLSearchParams(); - users.forEach((userId) => query.append('userId', userId)); - const response = await fetch(`/api/notifications/send/${notificator}?${query.toString()}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(message), - }); - if (response.ok) { - navigate(-1); - } else { - throw Error(await response.text()); - } - }, [users, notificator, message, navigate]); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['serverAnnouncement']}> - <Container maxWidth="xs" className={classes.container}> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - multiple - value={users} - onChange={(e) => setUsers(e.target.value)} - endpoint="/api/users" - label={t('settingsUsers')} - /> - <SelectField - value={notificator} - onChange={(e) => setNotificator(e.target.value)} - endpoint="/api/notifications/notificators" - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('notificator', it.type))} - label={t('notificationNotificators')} - /> - <TextField - value={message.subject} - onChange={(e) => setMessage({ ...message, subject: e.target.value })} - label={t('sharedSubject')} - /> - <TextField - value={message.body} - onChange={(e) => setMessage({ ...message, body: e.target.value })} - label={t('commandMessage')} - /> - </AccordionDetails> - </Accordion> - <div className={classes.buttons}> - <Button - type="button" - color="primary" - variant="outlined" - onClick={() => navigate(-1)} - > - {t('sharedCancel')} - </Button> - <Button - type="button" - color="primary" - variant="contained" - onClick={handleSend} - disabled={!notificator || !message.subject || !message.body} - > - {t('commandSend')} - </Button> - </div> - </Container> - </PageLayout> - ); -}; - -export default AnnouncementPage; diff --git a/modern/src/settings/CalendarPage.jsx b/modern/src/settings/CalendarPage.jsx deleted file mode 100644 index 8a3dc986..00000000 --- a/modern/src/settings/CalendarPage.jsx +++ /dev/null @@ -1,208 +0,0 @@ -import dayjs from 'dayjs'; -import React, { useState } from 'react'; -import { useDispatch } from 'react-redux'; -import TextField from '@mui/material/TextField'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, FormControl, InputLabel, Select, MenuItem, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { DropzoneArea } from 'react-mui-dropzone'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import { prefixString } from '../common/util/stringUtils'; -import { calendarsActions } from '../store'; -import { useCatch } from '../reactHelper'; -import useSettingsStyles from './common/useSettingsStyles'; - -const formatCalendarTime = (time) => { - const tzid = Intl.DateTimeFormat().resolvedOptions().timeZone; - return `TZID=${tzid}:${time.locale('en').format('YYYYMMDDTHHmmss')}`; -}; - -const parseRule = (rule) => { - if (rule.endsWith('COUNT=1')) { - return { frequency: 'ONCE' }; - } - const fragments = rule.split(';'); - const frequency = fragments[0].substring(11); - const by = fragments.length > 1 ? fragments[1].split('=')[1].split(',') : null; - return { frequency, by }; -}; - -const formatRule = (rule) => { - const by = rule.by && rule.by.join(','); - switch (rule.frequency) { - case 'DAILY': - return `RRULE:FREQ=${rule.frequency}`; - case 'WEEKLY': - return `RRULE:FREQ=${rule.frequency};BYDAY=${by || 'SU'}`; - case 'MONTHLY': - return `RRULE:FREQ=${rule.frequency};BYMONTHDAY=${by || 1}`; - default: - return 'RRULE:FREQ=DAILY;COUNT=1'; - } -}; - -const updateCalendar = (lines, index, element) => window.btoa(lines.map((e, i) => (i !== index ? e : element)).join('\n')); - -const simpleCalendar = () => window.btoa([ - 'BEGIN:VCALENDAR', - 'VERSION:2.0', - 'PRODID:-//Traccar//NONSGML Traccar//EN', - 'BEGIN:VEVENT', - 'UID:00000000-0000-0000-0000-000000000000', - `DTSTART;${formatCalendarTime(dayjs())}`, - `DTEND;${formatCalendarTime(dayjs().add(1, 'hours'))}`, - 'RRULE:FREQ=DAILY', - 'SUMMARY:Event', - 'END:VEVENT', - 'END:VCALENDAR', -].join('\n')); - -const CalendarPage = () => { - const classes = useSettingsStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const [item, setItem] = useState(); - - const decoded = item && item.data && window.atob(item.data); - - const simple = decoded && decoded.indexOf('//Traccar//') > 0; - - const lines = decoded && decoded.split('\n'); - - const rule = simple && parseRule(lines[7]); - - const handleFiles = (files) => { - if (files.length > 0) { - const reader = new FileReader(); - reader.onload = (event) => { - const { result } = event.target; - setItem({ ...item, data: result.substr(result.indexOf(',') + 1) }); - }; - reader.readAsDataURL(files[0]); - } - }; - - const onItemSaved = useCatch(async () => { - const response = await fetch('/api/calendars'); - if (response.ok) { - dispatch(calendarsActions.refresh(await response.json())); - } else { - throw Error(await response.text()); - } - }); - - const validate = () => item && item.name && item.data; - - return ( - <EditItemView - endpoint="calendars" - item={item} - setItem={setItem} - defaultItem={{ data: simpleCalendar() }} - validate={validate} - onItemSaved={onItemSaved} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedCalendar']} - > - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.name || ''} - onChange={(event) => setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - <FormControl> - <InputLabel>{t('sharedType')}</InputLabel> - <Select - label={t('sharedType')} - value={simple ? 'simple' : 'custom'} - onChange={(e) => setItem({ ...item, data: (e.target.value === 'simple' ? simpleCalendar() : null) })} - > - <MenuItem value="simple">{t('calendarSimple')}</MenuItem> - <MenuItem value="custom">{t('reportCustom')}</MenuItem> - </Select> - </FormControl> - {simple ? ( - <> - <TextField - label={t('reportFrom')} - type="datetime-local" - value={dayjs(lines[5].slice(-15)).locale('en').format('YYYY-MM-DDTHH:mm')} - onChange={(e) => { - const time = formatCalendarTime(dayjs(e.target.value, 'YYYY-MM-DDTHH:mm')); - setItem({ ...item, data: updateCalendar(lines, 5, `DTSTART;${time}`) }); - }} - /> - <TextField - label={t('reportTo')} - type="datetime-local" - value={dayjs(lines[6].slice(-15)).locale('en').format('YYYY-MM-DDTHH:mm')} - onChange={(e) => { - const time = formatCalendarTime(dayjs(e.target.value, 'YYYY-MM-DDTHH:mm')); - setItem({ ...item, data: updateCalendar(lines, 6, `DTEND;${time}`) }); - }} - /> - <FormControl> - <InputLabel>{t('calendarRecurrence')}</InputLabel> - <Select - label={t('calendarRecurrence')} - value={rule.frequency} - onChange={(e) => setItem({ ...item, data: updateCalendar(lines, 7, formatRule({ frequency: e.target.value })) })} - > - {['ONCE', 'DAILY', 'WEEKLY', 'MONTHLY'].map((it) => ( - <MenuItem key={it} value={it}>{t(prefixString('calendar', it.toLowerCase()))}</MenuItem> - ))} - </Select> - </FormControl> - {['WEEKLY', 'MONTHLY'].includes(rule.frequency) && ( - <FormControl> - <InputLabel>{t('calendarDays')}</InputLabel> - <Select - multiple - label={t('calendarDays')} - value={rule.by} - onChange={(e) => setItem({ ...item, data: updateCalendar(lines, 7, formatRule({ ...rule, by: e.target.value })) })} - > - {rule.frequency === 'WEEKLY' ? ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'].map((it) => ( - <MenuItem key={it} value={it.substring(0, 2).toUpperCase()}>{t(prefixString('calendar', it))}</MenuItem> - )) : Array.from({ length: 31 }, (_, i) => i + 1).map((it) => ( - <MenuItem key={it} value={it}>{it}</MenuItem> - ))} - </Select> - </FormControl> - )} - </> - ) : ( - <DropzoneArea - dropzoneText={t('sharedDropzoneText')} - filesLimit={1} - onChange={handleFiles} - showAlerts={false} - /> - )} - </AccordionDetails> - </Accordion> - <EditAttributesAccordion - attributes={item.attributes} - setAttributes={(attributes) => setItem({ ...item, attributes })} - definitions={{}} - /> - </> - )} - </EditItemView> - ); -}; - -export default CalendarPage; diff --git a/modern/src/settings/CalendarsPage.jsx b/modern/src/settings/CalendarsPage.jsx deleted file mode 100644 index de27a451..00000000 --- a/modern/src/settings/CalendarsPage.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const CalendarsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/calendars'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedCalendars']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('sharedName')}</TableCell> - <TableCell className={classes.columnAction} /> - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{item.name}</TableCell> - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions itemId={item.id} editPath="/settings/calendar" endpoint="calendars" setTimestamp={setTimestamp} /> - </TableCell> - </TableRow> - )) : (<TableShimmer columns={2} endAction />)} - </TableBody> - </Table> - <CollectionFab editPath="/settings/calendar" /> - </PageLayout> - ); -}; - -export default CalendarsPage; diff --git a/modern/src/settings/CommandDevicePage.jsx b/modern/src/settings/CommandDevicePage.jsx deleted file mode 100644 index b3144cd0..00000000 --- a/modern/src/settings/CommandDevicePage.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, - Button, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import BaseCommandView from './components/BaseCommandView'; -import SelectField from '../common/components/SelectField'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatch } from '../reactHelper'; -import { useRestriction } from '../common/util/permissions'; -import useSettingsStyles from './common/useSettingsStyles'; - -const CommandDevicePage = () => { - const navigate = useNavigate(); - const classes = useSettingsStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const [savedId, setSavedId] = useState(0); - const [item, setItem] = useState({}); - - const limitCommands = useRestriction('limitCommands'); - - const handleSend = useCatch(async () => { - let command; - if (savedId) { - const response = await fetch(`/api/commands/${savedId}`); - if (response.ok) { - command = await response.json(); - } else { - throw Error(await response.text()); - } - } else { - command = item; - } - - command.deviceId = parseInt(id, 10); - - const response = await fetch('/api/commands/send', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(command), - }); - - if (response.ok) { - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - const validate = () => savedId || (item && item.type); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'deviceCommand']}> - <Container maxWidth="xs" className={classes.container}> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - value={savedId} - emptyValue={limitCommands ? null : 0} - emptyTitle={t('sharedNew')} - onChange={(e) => setSavedId(e.target.value)} - endpoint={`/api/commands/send?deviceId=${id}`} - titleGetter={(it) => it.description} - label={t('sharedSavedCommand')} - /> - {!limitCommands && !savedId && ( - <BaseCommandView deviceId={id} item={item} setItem={setItem} /> - )} - </AccordionDetails> - </Accordion> - <div className={classes.buttons}> - <Button - type="button" - color="primary" - variant="outlined" - onClick={() => navigate(-1)} - > - {t('sharedCancel')} - </Button> - <Button - type="button" - color="primary" - variant="contained" - onClick={handleSend} - disabled={!validate()} - > - {t('commandSend')} - </Button> - </div> - </Container> - </PageLayout> - ); -}; - -export default CommandDevicePage; diff --git a/modern/src/settings/CommandGroupPage.jsx b/modern/src/settings/CommandGroupPage.jsx deleted file mode 100644 index e55a235d..00000000 --- a/modern/src/settings/CommandGroupPage.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, - Button, - FormControl, - InputLabel, - Select, - MenuItem, - FormControlLabel, - Checkbox, - TextField, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatch } from '../reactHelper'; -import useSettingsStyles from './common/useSettingsStyles'; - -const CommandDevicePage = () => { - const navigate = useNavigate(); - const classes = useSettingsStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const textEnabled = useSelector((state) => state.session.server.textEnabled); - - const [item, setItem] = useState({ type: 'custom', attributes: {} }); - - const handleSend = useCatch(async () => { - const query = new URLSearchParams({ groupId: id }); - const response = await fetch(`/api/commands/send?${query.toString()}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'deviceCommand']}> - <Container maxWidth="xs" className={classes.container}> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <FormControl fullWidth> - <InputLabel>{t('sharedType')}</InputLabel> - <Select label={t('sharedType')} value="custom" disabled> - <MenuItem value="custom">{t('commandCustom')}</MenuItem> - </Select> - </FormControl> - <TextField - value={item.attributes.data} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, data: e.target.value } })} - label={t('commandData')} - /> - {textEnabled && ( - <FormControlLabel - control={<Checkbox checked={item.textChannel} onChange={(event) => setItem({ ...item, textChannel: event.target.checked })} />} - label={t('commandSendSms')} - /> - )} - </AccordionDetails> - </Accordion> - <div className={classes.buttons}> - <Button - type="button" - color="primary" - variant="outlined" - onClick={() => navigate(-1)} - > - {t('sharedCancel')} - </Button> - <Button - type="button" - color="primary" - variant="contained" - onClick={handleSend} - disabled={!item.attributes.data} - > - {t('commandSend')} - </Button> - </div> - </Container> - </PageLayout> - ); -}; - -export default CommandDevicePage; diff --git a/modern/src/settings/CommandPage.jsx b/modern/src/settings/CommandPage.jsx deleted file mode 100644 index e65ecd76..00000000 --- a/modern/src/settings/CommandPage.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useState } from 'react'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, TextField, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import BaseCommandView from './components/BaseCommandView'; -import SettingsMenu from './components/SettingsMenu'; -import useSettingsStyles from './common/useSettingsStyles'; - -const CommandPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [item, setItem] = useState(); - - const validate = () => item && item.type; - - return ( - <EditItemView - endpoint="commands" - item={item} - setItem={setItem} - validate={validate} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedSavedCommand']} - > - {item && ( - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.description || ''} - onChange={(event) => setItem({ ...item, description: event.target.value })} - label={t('sharedDescription')} - /> - <BaseCommandView item={item} setItem={setItem} /> - </AccordionDetails> - </Accordion> - )} - </EditItemView> - ); -}; - -export default CommandPage; diff --git a/modern/src/settings/CommandsPage.jsx b/modern/src/settings/CommandsPage.jsx deleted file mode 100644 index 1b893831..00000000 --- a/modern/src/settings/CommandsPage.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import { formatBoolean } from '../common/util/formatter'; -import { prefixString } from '../common/util/stringUtils'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import { useRestriction } from '../common/util/permissions'; -import useSettingsStyles from './common/useSettingsStyles'; - -const CommandsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - const limitCommands = useRestriction('limitCommands'); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/commands'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedSavedCommands']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('sharedDescription')}</TableCell> - <TableCell>{t('sharedType')}</TableCell> - <TableCell>{t('commandSendSms')}</TableCell> - {!limitCommands && <TableCell className={classes.columnAction} />} - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{item.description}</TableCell> - <TableCell>{t(prefixString('command', item.type))}</TableCell> - <TableCell>{formatBoolean(item.textChannel, t)}</TableCell> - {!limitCommands && ( - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions itemId={item.id} editPath="/settings/command" endpoint="commands" setTimestamp={setTimestamp} /> - </TableCell> - )} - </TableRow> - )) : (<TableShimmer columns={limitCommands ? 3 : 4} endAction />)} - </TableBody> - </Table> - <CollectionFab editPath="/settings/command" disabled={limitCommands} /> - </PageLayout> - ); -}; - -export default CommandsPage; diff --git a/modern/src/settings/ComputedAttributePage.jsx b/modern/src/settings/ComputedAttributePage.jsx deleted file mode 100644 index 1b19033c..00000000 --- a/modern/src/settings/ComputedAttributePage.jsx +++ /dev/null @@ -1,177 +0,0 @@ -import React, { useState } from 'react'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - FormControl, - InputLabel, - MenuItem, - Select, - TextField, - createFilterOptions, - Autocomplete, - Button, - Snackbar, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import SettingsMenu from './components/SettingsMenu'; -import SelectField from '../common/components/SelectField'; -import { useCatch } from '../reactHelper'; -import { snackBarDurationLongMs } from '../common/util/duration'; -import useSettingsStyles from './common/useSettingsStyles'; - -const allowedProperties = ['valid', 'latitude', 'longitude', 'altitude', 'speed', 'course', 'address', 'accuracy']; - -const ComputedAttributePage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const positionAttributes = usePositionAttributes(t); - - const [item, setItem] = useState(); - const [deviceId, setDeviceId] = useState(); - const [result, setResult] = useState(); - - const options = Object.entries(positionAttributes).filter(([key, value]) => !value.property || allowedProperties.includes(key)).map(([key, value]) => ({ - key, - name: value.name, - type: value.type, - })); - - const filter = createFilterOptions({ - stringify: (option) => option.name, - }); - - const testAttribute = useCatch(async () => { - const query = new URLSearchParams({ deviceId }); - const url = `/api/attributes/computed/test?${query.toString()}`; - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - if (response.ok) { - setResult(await response.text()); - } else { - throw Error(await response.text()); - } - }); - - const validate = () => item && item.description && item.expression; - - return ( - <EditItemView - endpoint="attributes/computed" - item={item} - setItem={setItem} - validate={validate} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedComputedAttribute']} - > - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.description || ''} - onChange={(e) => setItem({ ...item, description: e.target.value })} - label={t('sharedDescription')} - /> - <Autocomplete - value={options.find((option) => option.key === item.attribute) || item.attribute} - onChange={(_, option) => { - const attribute = option ? option.key || option : null; - if (option && option.type) { - setItem({ ...item, attribute, type: option.type }); - } else { - setItem({ ...item, attribute }); - } - }} - filterOptions={(options, params) => { - const filtered = filter(options, params); - if (params.inputValue) { - filtered.push({ - key: params.inputValue, - name: params.inputValue, - }); - } - return filtered; - }} - options={options} - getOptionLabel={(option) => option.name || option} - renderOption={(props, option) => ( - <li {...props}> - {option.name} - </li> - )} - renderInput={(params) => ( - <TextField {...params} label={t('sharedAttribute')} /> - )} - freeSolo - /> - <TextField - value={item.expression || ''} - onChange={(e) => setItem({ ...item, expression: e.target.value })} - label={t('sharedExpression')} - multiline - rows={4} - /> - <FormControl disabled={item.attribute in positionAttributes}> - <InputLabel>{t('sharedType')}</InputLabel> - <Select - label={t('sharedType')} - value={item.type || ''} - onChange={(e) => setItem({ ...item, type: e.target.value })} - > - <MenuItem value="string">{t('sharedTypeString')}</MenuItem> - <MenuItem value="number">{t('sharedTypeNumber')}</MenuItem> - <MenuItem value="boolean">{t('sharedTypeBoolean')}</MenuItem> - </Select> - </FormControl> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedTest')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - value={deviceId} - onChange={(e) => setDeviceId(Number(e.target.value))} - endpoint="/api/devices" - label={t('sharedDevice')} - /> - <Button - variant="outlined" - color="primary" - onClick={testAttribute} - disabled={!deviceId} - > - {t('sharedTestExpression')} - </Button> - <Snackbar - open={!!result} - onClose={() => setResult(null)} - autoHideDuration={snackBarDurationLongMs} - message={result} - /> - </AccordionDetails> - </Accordion> - </> - )} - </EditItemView> - ); -}; - -export default ComputedAttributePage; diff --git a/modern/src/settings/ComputedAttributesPage.jsx b/modern/src/settings/ComputedAttributesPage.jsx deleted file mode 100644 index 6d098547..00000000 --- a/modern/src/settings/ComputedAttributesPage.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import { useAdministrator } from '../common/util/permissions'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const ComputedAttributesPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - const administrator = useAdministrator(); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/attributes/computed'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedComputedAttributes']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('sharedDescription')}</TableCell> - <TableCell>{t('sharedAttribute')}</TableCell> - <TableCell>{t('sharedExpression')}</TableCell> - <TableCell>{t('sharedType')}</TableCell> - {administrator && <TableCell className={classes.columnAction} />} - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{item.description}</TableCell> - <TableCell>{item.attribute}</TableCell> - <TableCell>{item.expression}</TableCell> - <TableCell>{item.type}</TableCell> - {administrator && ( - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions itemId={item.id} editPath="/settings/attribute" endpoint="attributes/computed" setTimestamp={setTimestamp} /> - </TableCell> - )} - </TableRow> - )) : (<TableShimmer columns={administrator ? 5 : 4} endAction={administrator} />)} - </TableBody> - </Table> - <CollectionFab editPath="/settings/attribute" disabled={!administrator} /> - </PageLayout> - ); -}; - -export default ComputedAttributesPage; diff --git a/modern/src/settings/DeviceConnectionsPage.jsx b/modern/src/settings/DeviceConnectionsPage.jsx deleted file mode 100644 index c711d719..00000000 --- a/modern/src/settings/DeviceConnectionsPage.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import LinkField from '../common/components/LinkField'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import { formatNotificationTitle } from '../common/util/formatter'; -import PageLayout from '../common/components/PageLayout'; -import useFeatures from '../common/util/useFeatures'; -import useSettingsStyles from './common/useSettingsStyles'; - -const DeviceConnectionsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const features = useFeatures(); - - return ( - <PageLayout - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedDevice', 'sharedConnections']} - > - <Container maxWidth="xs" className={classes.container}> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedConnections')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <LinkField - endpointAll="/api/geofences" - endpointLinked={`/api/geofences?deviceId=${id}`} - baseId={id} - keyBase="deviceId" - keyLink="geofenceId" - label={t('sharedGeofences')} - /> - <LinkField - endpointAll="/api/notifications" - endpointLinked={`/api/notifications?deviceId=${id}`} - baseId={id} - keyBase="deviceId" - keyLink="notificationId" - titleGetter={(it) => formatNotificationTitle(t, it)} - label={t('sharedNotifications')} - /> - {!features.disableDrivers && ( - <LinkField - endpointAll="/api/drivers" - endpointLinked={`/api/drivers?deviceId=${id}`} - baseId={id} - keyBase="deviceId" - keyLink="driverId" - titleGetter={(it) => `${it.name} (${it.uniqueId})`} - label={t('sharedDrivers')} - /> - )} - {!features.disableComputedAttributes && ( - <LinkField - endpointAll="/api/attributes/computed" - endpointLinked={`/api/attributes/computed?deviceId=${id}`} - baseId={id} - keyBase="deviceId" - keyLink="attributeId" - titleGetter={(it) => it.description} - label={t('sharedComputedAttributes')} - /> - )} - {!features.disableSavedCommands && ( - <LinkField - endpointAll="/api/commands" - endpointLinked={`/api/commands?deviceId=${id}`} - baseId={id} - keyBase="deviceId" - keyLink="commandId" - titleGetter={(it) => it.description} - label={t('sharedSavedCommands')} - /> - )} - {!features.disableMaintenance && ( - <LinkField - endpointAll="/api/maintenance" - endpointLinked={`/api/maintenance?deviceId=${id}`} - baseId={id} - keyBase="deviceId" - keyLink="maintenanceId" - label={t('sharedMaintenance')} - /> - )} - </AccordionDetails> - </Accordion> - </Container> - </PageLayout> - ); -}; - -export default DeviceConnectionsPage; diff --git a/modern/src/settings/DevicePage.jsx b/modern/src/settings/DevicePage.jsx deleted file mode 100644 index 8933a210..00000000 --- a/modern/src/settings/DevicePage.jsx +++ /dev/null @@ -1,176 +0,0 @@ -import React, { useState } from 'react'; -import dayjs from 'dayjs'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - FormControlLabel, - Checkbox, - TextField, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { DropzoneArea } from 'react-mui-dropzone'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import SelectField from '../common/components/SelectField'; -import deviceCategories from '../common/util/deviceCategories'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import useDeviceAttributes from '../common/attributes/useDeviceAttributes'; -import { useAdministrator } from '../common/util/permissions'; -import SettingsMenu from './components/SettingsMenu'; -import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; -import { useCatch } from '../reactHelper'; -import useQuery from '../common/util/useQuery'; -import useSettingsStyles from './common/useSettingsStyles'; - -const DevicePage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const admin = useAdministrator(); - - const commonDeviceAttributes = useCommonDeviceAttributes(t); - const deviceAttributes = useDeviceAttributes(t); - - const query = useQuery(); - const uniqueId = query.get('uniqueId'); - - const [item, setItem] = useState(uniqueId ? { uniqueId } : null); - - const handleFiles = useCatch(async (files) => { - if (files.length > 0) { - const response = await fetch(`/api/devices/${item.id}/image`, { - method: 'POST', - body: files[0], - }); - if (response.ok) { - setItem({ ...item, attributes: { ...item.attributes, deviceImage: await response.text() } }); - } else { - throw Error(await response.text()); - } - } - }); - - const validate = () => item && item.name && item.uniqueId; - - return ( - <EditItemView - endpoint="devices" - item={item} - setItem={setItem} - validate={validate} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedDevice']} - > - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.name || ''} - onChange={(event) => setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - <TextField - value={item.uniqueId || ''} - onChange={(event) => setItem({ ...item, uniqueId: event.target.value })} - label={t('deviceIdentifier')} - helperText={t('deviceIdentifierHelp')} - disabled={Boolean(uniqueId)} - /> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedExtra')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - value={item.groupId} - onChange={(event) => setItem({ ...item, groupId: Number(event.target.value) })} - endpoint="/api/groups" - label={t('groupParent')} - /> - <TextField - value={item.phone || ''} - onChange={(event) => setItem({ ...item, phone: event.target.value })} - label={t('sharedPhone')} - /> - <TextField - value={item.model || ''} - onChange={(event) => setItem({ ...item, model: event.target.value })} - label={t('deviceModel')} - /> - <TextField - value={item.contact || ''} - onChange={(event) => setItem({ ...item, contact: event.target.value })} - label={t('deviceContact')} - /> - <SelectField - value={item.category || 'default'} - onChange={(event) => setItem({ ...item, category: event.target.value })} - data={deviceCategories.map((category) => ({ - id: category, - name: t(`category${category.replace(/^\w/, (c) => c.toUpperCase())}`), - }))} - label={t('deviceCategory')} - /> - <SelectField - value={item.calendarId} - onChange={(event) => setItem({ ...item, calendarId: Number(event.target.value) })} - endpoint="/api/calendars" - label={t('sharedCalendar')} - /> - <TextField - label={t('userExpirationTime')} - type="date" - value={(item.expirationTime && dayjs(item.expirationTime).locale('en').format('YYYY-MM-DD')) || '2099-01-01'} - onChange={(e) => setItem({ ...item, expirationTime: dayjs(e.target.value, 'YYYY-MM-DD').locale('en').format() })} - disabled={!admin} - /> - <FormControlLabel - control={<Checkbox checked={item.disabled} onChange={(event) => setItem({ ...item, disabled: event.target.checked })} />} - label={t('sharedDisabled')} - disabled={!admin} - /> - </AccordionDetails> - </Accordion> - {item.id && ( - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('attributeDeviceImage')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <DropzoneArea - dropzoneText={t('sharedDropzoneText')} - acceptedFiles={['image/*']} - filesLimit={1} - onChange={handleFiles} - showAlerts={false} - maxFileSize={500000} - /> - </AccordionDetails> - </Accordion> - )} - <EditAttributesAccordion - attributes={item.attributes} - setAttributes={(attributes) => setItem({ ...item, attributes })} - definitions={{ ...commonDeviceAttributes, ...deviceAttributes }} - /> - </> - )} - </EditItemView> - ); -}; - -export default DevicePage; diff --git a/modern/src/settings/DevicesPage.jsx b/modern/src/settings/DevicesPage.jsx deleted file mode 100644 index c0da0ba7..00000000 --- a/modern/src/settings/DevicesPage.jsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate } from 'react-router-dom'; -import { - Table, TableRow, TableCell, TableHead, TableBody, Button, TableFooter, -} from '@mui/material'; -import LinkIcon from '@mui/icons-material/Link'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import { usePreference } from '../common/util/preferences'; -import { formatTime } from '../common/util/formatter'; -import { useDeviceReadonly } from '../common/util/permissions'; -import useSettingsStyles from './common/useSettingsStyles'; - -const DevicesPage = () => { - const classes = useSettingsStyles(); - const navigate = useNavigate(); - const t = useTranslation(); - - const groups = useSelector((state) => state.groups.items); - - const hours12 = usePreference('twelveHourFormat'); - - const deviceReadonly = useDeviceReadonly(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/devices'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const handleExport = () => { - window.location.assign('/api/reports/devices/xlsx'); - }; - - const actionConnections = { - key: 'connections', - title: t('sharedConnections'), - icon: <LinkIcon fontSize="small" />, - handler: (deviceId) => navigate(`/settings/device/${deviceId}/connections`), - }; - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'deviceTitle']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('sharedName')}</TableCell> - <TableCell>{t('deviceIdentifier')}</TableCell> - <TableCell>{t('groupParent')}</TableCell> - <TableCell>{t('sharedPhone')}</TableCell> - <TableCell>{t('deviceModel')}</TableCell> - <TableCell>{t('deviceContact')}</TableCell> - <TableCell>{t('userExpirationTime')}</TableCell> - <TableCell className={classes.columnAction} /> - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{item.name}</TableCell> - <TableCell>{item.uniqueId}</TableCell> - <TableCell>{item.groupId ? groups[item.groupId]?.name : null}</TableCell> - <TableCell>{item.phone}</TableCell> - <TableCell>{item.model}</TableCell> - <TableCell>{item.contact}</TableCell> - <TableCell>{formatTime(item.expirationTime, 'date', hours12)}</TableCell> - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions - itemId={item.id} - editPath="/settings/device" - endpoint="devices" - setTimestamp={setTimestamp} - customActions={[actionConnections]} - readonly={deviceReadonly} - /> - </TableCell> - </TableRow> - )) : (<TableShimmer columns={7} endAction />)} - </TableBody> - <TableFooter> - <TableRow> - <TableCell colSpan={8} align="right"> - <Button onClick={handleExport} variant="text">{t('reportExport')}</Button> - </TableCell> - </TableRow> - </TableFooter> - </Table> - <CollectionFab editPath="/settings/device" /> - </PageLayout> - ); -}; - -export default DevicesPage; diff --git a/modern/src/settings/DriverPage.jsx b/modern/src/settings/DriverPage.jsx deleted file mode 100644 index 5f70a44a..00000000 --- a/modern/src/settings/DriverPage.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useState } from 'react'; -import TextField from '@mui/material/TextField'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import useSettingsStyles from './common/useSettingsStyles'; - -const DriverPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [item, setItem] = useState(); - - const validate = () => item && item.name && item.uniqueId; - - return ( - <EditItemView - endpoint="drivers" - item={item} - setItem={setItem} - validate={validate} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedDriver']} - > - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.name || ''} - onChange={(event) => setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - <TextField - value={item.uniqueId || ''} - onChange={(event) => setItem({ ...item, uniqueId: event.target.value })} - label={t('deviceIdentifier')} - /> - </AccordionDetails> - </Accordion> - <EditAttributesAccordion - attributes={item.attributes} - setAttributes={(attributes) => setItem({ ...item, attributes })} - definitions={{}} - /> - </> - )} - </EditItemView> - ); -}; - -export default DriverPage; diff --git a/modern/src/settings/DriversPage.jsx b/modern/src/settings/DriversPage.jsx deleted file mode 100644 index 72834860..00000000 --- a/modern/src/settings/DriversPage.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const DriversPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/drivers'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedDrivers']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('sharedName')}</TableCell> - <TableCell>{t('deviceIdentifier')}</TableCell> - <TableCell className={classes.columnAction} /> - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{item.name}</TableCell> - <TableCell>{item.uniqueId}</TableCell> - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions itemId={item.id} editPath="/settings/driver" endpoint="drivers" setTimestamp={setTimestamp} /> - </TableCell> - </TableRow> - )) : (<TableShimmer columns={3} endAction />)} - </TableBody> - </Table> - <CollectionFab editPath="/settings/driver" /> - </PageLayout> - ); -}; - -export default DriversPage; diff --git a/modern/src/settings/GeofencePage.jsx b/modern/src/settings/GeofencePage.jsx deleted file mode 100644 index c3c96ef8..00000000 --- a/modern/src/settings/GeofencePage.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, TextField, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import useGeofenceAttributes from '../common/attributes/useGeofenceAttributes'; -import SettingsMenu from './components/SettingsMenu'; -import SelectField from '../common/components/SelectField'; -import { geofencesActions } from '../store'; -import useSettingsStyles from './common/useSettingsStyles'; - -const GeofencePage = () => { - const classes = useSettingsStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const geofenceAttributes = useGeofenceAttributes(t); - - const [item, setItem] = useState(); - - const onItemSaved = (result) => { - dispatch(geofencesActions.update([result])); - }; - - const validate = () => item && item.name; - - return ( - <EditItemView - endpoint="geofences" - item={item} - setItem={setItem} - validate={validate} - onItemSaved={onItemSaved} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedGeofence']} - > - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.name || ''} - onChange={(event) => setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedExtra')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.description || ''} - onChange={(event) => setItem({ ...item, description: event.target.value })} - label={t('sharedDescription')} - /> - <SelectField - value={item.calendarId} - onChange={(event) => setItem({ ...item, calendarId: Number(event.target.value) })} - endpoint="/api/calendars" - label={t('sharedCalendar')} - /> - </AccordionDetails> - </Accordion> - <EditAttributesAccordion - attributes={item.attributes} - setAttributes={(attributes) => setItem({ ...item, attributes })} - definitions={geofenceAttributes} - /> - </> - )} - </EditItemView> - ); -}; - -export default GeofencePage; diff --git a/modern/src/settings/GroupConnectionsPage.jsx b/modern/src/settings/GroupConnectionsPage.jsx deleted file mode 100644 index 980bd9da..00000000 --- a/modern/src/settings/GroupConnectionsPage.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import LinkField from '../common/components/LinkField'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import { formatNotificationTitle } from '../common/util/formatter'; -import PageLayout from '../common/components/PageLayout'; -import useFeatures from '../common/util/useFeatures'; -import useSettingsStyles from './common/useSettingsStyles'; - -const GroupConnectionsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const features = useFeatures(); - - return ( - <PageLayout - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'groupDialog', 'sharedConnections']} - > - <Container maxWidth="xs" className={classes.container}> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedConnections')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <LinkField - endpointAll="/api/geofences" - endpointLinked={`/api/geofences?groupId=${id}`} - baseId={id} - keyBase="groupId" - keyLink="geofenceId" - label={t('sharedGeofences')} - /> - <LinkField - endpointAll="/api/notifications" - endpointLinked={`/api/notifications?groupId=${id}`} - baseId={id} - keyBase="groupId" - keyLink="notificationId" - titleGetter={(it) => formatNotificationTitle(t, it)} - label={t('sharedNotifications')} - /> - {!features.disableDrivers && ( - <LinkField - endpointAll="/api/drivers" - endpointLinked={`/api/drivers?groupId=${id}`} - baseId={id} - keyBase="groupId" - keyLink="driverId" - titleGetter={(it) => `${it.name} (${it.uniqueId})`} - label={t('sharedDrivers')} - /> - )} - {!features.disableComputedAttributes && ( - <LinkField - endpointAll="/api/attributes/computed" - endpointLinked={`/api/attributes/computed?groupId=${id}`} - baseId={id} - keyBase="groupId" - keyLink="attributeId" - titleGetter={(it) => it.description} - label={t('sharedComputedAttributes')} - /> - )} - {!features.disableSavedCommands && ( - <LinkField - endpointAll="/api/commands" - endpointLinked={`/api/commands?groupId=${id}`} - baseId={id} - keyBase="groupId" - keyLink="commandId" - titleGetter={(it) => it.description} - label={t('sharedSavedCommands')} - /> - )} - {!features.disableMaintenance && ( - <LinkField - endpointAll="/api/maintenance" - endpointLinked={`/api/maintenance?groupId=${id}`} - baseId={id} - keyBase="groupId" - keyLink="maintenanceId" - label={t('sharedMaintenance')} - /> - )} - </AccordionDetails> - </Accordion> - </Container> - </PageLayout> - ); -}; - -export default GroupConnectionsPage; diff --git a/modern/src/settings/GroupPage.jsx b/modern/src/settings/GroupPage.jsx deleted file mode 100644 index ba1cbc76..00000000 --- a/modern/src/settings/GroupPage.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { useState } from 'react'; -import { useDispatch } from 'react-redux'; -import TextField from '@mui/material/TextField'; - -import { - Accordion, AccordionSummary, AccordionDetails, Typography, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import SelectField from '../common/components/SelectField'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; -import useGroupAttributes from '../common/attributes/useGroupAttributes'; -import { useCatch } from '../reactHelper'; -import { groupsActions } from '../store'; -import useSettingsStyles from './common/useSettingsStyles'; - -const GroupPage = () => { - const classes = useSettingsStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const commonDeviceAttributes = useCommonDeviceAttributes(t); - const groupAttributes = useGroupAttributes(t); - - const [item, setItem] = useState(); - - const onItemSaved = useCatch(async () => { - const response = await fetch('/api/groups'); - if (response.ok) { - dispatch(groupsActions.refresh(await response.json())); - } else { - throw Error(await response.text()); - } - }); - - const validate = () => item && item.name; - - return ( - <EditItemView - endpoint="groups" - item={item} - setItem={setItem} - validate={validate} - onItemSaved={onItemSaved} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'groupDialog']} - > - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.name || ''} - onChange={(event) => setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedExtra')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - value={item.groupId} - onChange={(event) => setItem({ ...item, groupId: Number(event.target.value) })} - endpoint="/api/groups" - label={t('groupParent')} - /> - </AccordionDetails> - </Accordion> - <EditAttributesAccordion - attributes={item.attributes} - setAttributes={(attributes) => setItem({ ...item, attributes })} - definitions={{ ...commonDeviceAttributes, ...groupAttributes }} - /> - </> - )} - </EditItemView> - ); -}; - -export default GroupPage; diff --git a/modern/src/settings/GroupsPage.jsx b/modern/src/settings/GroupsPage.jsx deleted file mode 100644 index baba7f72..00000000 --- a/modern/src/settings/GroupsPage.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import LinkIcon from '@mui/icons-material/Link'; -import PublishIcon from '@mui/icons-material/Publish'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import { useRestriction } from '../common/util/permissions'; -import useSettingsStyles from './common/useSettingsStyles'; - -const GroupsPage = () => { - const classes = useSettingsStyles(); - const navigate = useNavigate(); - const t = useTranslation(); - - const limitCommands = useRestriction('limitCommands'); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/groups'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const actionCommand = { - key: 'command', - title: t('deviceCommand'), - icon: <PublishIcon fontSize="small" />, - handler: (groupId) => navigate(`/settings/group/${groupId}/command`), - }; - - const actionConnections = { - key: 'connections', - title: t('sharedConnections'), - icon: <LinkIcon fontSize="small" />, - handler: (groupId) => navigate(`/settings/group/${groupId}/connections`), - }; - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'settingsGroups']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('sharedName')}</TableCell> - <TableCell className={classes.columnAction} /> - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{item.name}</TableCell> - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions - itemId={item.id} - editPath="/settings/group" - endpoint="groups" - setTimestamp={setTimestamp} - customActions={limitCommands ? [actionConnections] : [actionConnections, actionCommand]} - /> - </TableCell> - </TableRow> - )) : (<TableShimmer columns={2} endAction />)} - </TableBody> - </Table> - <CollectionFab editPath="/settings/group" /> - </PageLayout> - ); -}; - -export default GroupsPage; diff --git a/modern/src/settings/MaintenancePage.jsx b/modern/src/settings/MaintenancePage.jsx deleted file mode 100644 index 491a0d3b..00000000 --- a/modern/src/settings/MaintenancePage.jsx +++ /dev/null @@ -1,174 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import dayjs from 'dayjs'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - TextField, - FormControl, - InputLabel, - MenuItem, - Select, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { prefixString } from '../common/util/stringUtils'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useAttributePreference } from '../common/util/preferences'; -import { - speedFromKnots, speedToKnots, distanceFromMeters, distanceToMeters, -} from '../common/util/converter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import SettingsMenu from './components/SettingsMenu'; -import useSettingsStyles from './common/useSettingsStyles'; - -const MaintenancePage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const positionAttributes = usePositionAttributes(t); - - const [item, setItem] = useState(); - const [labels, setLabels] = useState({ start: '', period: '' }); - - const speedUnit = useAttributePreference('speedUnit', 'kn'); - const distanceUnit = useAttributePreference('distanceUnit', 'km'); - - const convertToList = (attributes) => { - const otherList = []; - Object.keys(attributes).forEach((key) => { - const value = attributes[key]; - if (value.type === 'number' || key.endsWith('Time')) { - otherList.push({ key, name: value.name, type: value.type }); - } - }); - return otherList; - }; - - useEffect(() => { - const attribute = positionAttributes[item?.type]; - if (item?.type?.endsWith('Time')) { - setLabels({ ...labels, start: null, period: t('sharedDays') }); - } else if (attribute && attribute.dataType) { - switch (attribute.dataType) { - case 'distance': - setLabels({ ...labels, start: t(prefixString('shared', distanceUnit)), period: t(prefixString('shared', distanceUnit)) }); - break; - case 'speed': - setLabels({ ...labels, start: t(prefixString('shared', speedUnit)), period: t(prefixString('shared', speedUnit)) }); - break; - default: - setLabels({ ...labels, start: null, period: null }); - break; - } - } else { - setLabels({ ...labels, start: null, period: null }); - } - }, [item?.type]); - - const rawToValue = (start, value) => { - const attribute = positionAttributes[item.type]; - if (item.type?.endsWith('Time')) { - if (start) { - return dayjs(value).locale('en').format('YYYY-MM-DD'); - } - return value / 86400000; - } - if (attribute && attribute.dataType) { - switch (attribute.dataType) { - case 'speed': - return speedFromKnots(value, speedUnit); - case 'distance': - return distanceFromMeters(value, distanceUnit); - default: - return value; - } - } - return value; - }; - - const valueToRaw = (start, value) => { - const attribute = positionAttributes[item.type]; - if (item.type?.endsWith('Time')) { - if (start) { - return dayjs(value, 'YYYY-MM-DD').valueOf(); - } - return value * 86400000; - } if (attribute && attribute.dataType) { - switch (attribute.dataType) { - case 'speed': - return speedToKnots(value, speedUnit); - case 'distance': - return distanceToMeters(value, distanceUnit); - default: - return value; - } - } - return value; - }; - - const validate = () => item && item.name && item.type && item.start && item.period; - - return ( - <EditItemView - endpoint="maintenance" - item={item} - setItem={setItem} - validate={validate} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedMaintenance']} - > - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.name || ''} - onChange={(e) => setItem({ ...item, name: e.target.value })} - label={t('sharedName')} - /> - <FormControl> - <InputLabel>{t('sharedType')}</InputLabel> - <Select - label={t('sharedType')} - value={item.type || ''} - onChange={(e) => setItem({ ...item, type: e.target.value, start: 0, period: 0 })} - > - {convertToList(positionAttributes).map(({ key, name }) => ( - <MenuItem key={key} value={key}>{name}</MenuItem> - ))} - </Select> - </FormControl> - <TextField - type={item.type?.endsWith('Time') ? 'date' : 'number'} - value={rawToValue(true, item.start) || ''} - onChange={(e) => setItem({ ...item, start: valueToRaw(true, e.target.value) })} - label={labels.start ? `${t('maintenanceStart')} (${labels.start})` : t('maintenanceStart')} - /> - <TextField - type="number" - value={rawToValue(false, item.period) || ''} - onChange={(e) => setItem({ ...item, period: valueToRaw(false, e.target.value) })} - label={labels.period ? `${t('maintenancePeriod')} (${labels.period})` : t('maintenancePeriod')} - /> - </AccordionDetails> - </Accordion> - <EditAttributesAccordion - attributes={item.attributes} - setAttributes={(attributes) => setItem({ ...item, attributes })} - definitions={{}} - /> - </> - )} - </EditItemView> - ); -}; - -export default MaintenancePage; diff --git a/modern/src/settings/MaintenancesPage.jsx b/modern/src/settings/MaintenancesPage.jsx deleted file mode 100644 index 9241eb3e..00000000 --- a/modern/src/settings/MaintenancesPage.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useState } from 'react'; -import dayjs from 'dayjs'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import { formatDistance, formatSpeed } from '../common/util/formatter'; -import { useAttributePreference } from '../common/util/preferences'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const MaintenacesPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const positionAttributes = usePositionAttributes(t); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - const speedUnit = useAttributePreference('speedUnit'); - const distanceUnit = useAttributePreference('distanceUnit'); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/maintenance'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const convertAttribute = (key, start, value) => { - const attribute = positionAttributes[key]; - if (key.endsWith('Time')) { - if (start) { - return dayjs(value).locale('en').format('YYYY-MM-DD'); - } - return `${value / 86400000} ${t('sharedDays')}`; - } - if (attribute && attribute.dataType) { - switch (attribute.dataType) { - case 'speed': - return formatSpeed(value, speedUnit, t); - case 'distance': - return formatDistance(value, distanceUnit, t); - default: - return value; - } - } - - return value; - }; - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedMaintenance']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('sharedName')}</TableCell> - <TableCell>{t('sharedType')}</TableCell> - <TableCell>{t('maintenanceStart')}</TableCell> - <TableCell>{t('maintenancePeriod')}</TableCell> - <TableCell className={classes.columnAction} /> - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{item.name}</TableCell> - <TableCell>{item.type}</TableCell> - <TableCell>{convertAttribute(item.type, true, item.start)}</TableCell> - <TableCell>{convertAttribute(item.type, false, item.period)}</TableCell> - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions itemId={item.id} editPath="/settings/maintenance" endpoint="maintenance" setTimestamp={setTimestamp} /> - </TableCell> - </TableRow> - )) : (<TableShimmer columns={5} endAction />)} - </TableBody> - </Table> - <CollectionFab editPath="/settings/maintenance" /> - </PageLayout> - ); -}; - -export default MaintenacesPage; diff --git a/modern/src/settings/NotificationPage.jsx b/modern/src/settings/NotificationPage.jsx deleted file mode 100644 index 63aa9b95..00000000 --- a/modern/src/settings/NotificationPage.jsx +++ /dev/null @@ -1,144 +0,0 @@ -import React, { useState } from 'react'; - -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - FormControlLabel, - Checkbox, - FormGroup, - Button, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider'; -import EditItemView from './components/EditItemView'; -import { prefixString, unprefixString } from '../common/util/stringUtils'; -import SelectField from '../common/components/SelectField'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatch } from '../reactHelper'; -import useSettingsStyles from './common/useSettingsStyles'; - -const NotificationPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [item, setItem] = useState(); - - const alarms = useTranslationKeys((it) => it.startsWith('alarm')).map((it) => ({ - key: unprefixString('alarm', it), - name: t(it), - })); - - const testNotificators = useCatch(async () => { - await Promise.all(item.notificators.split(/[, ]+/).map(async (notificator) => { - const response = await fetch(`/api/notifications/test/${notificator}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - if (!response.ok) { - throw Error(await response.text()); - } - })); - }); - - const validate = () => item && item.type && item.notificators && (!item.notificators?.includes('command') || item.commandId); - - return ( - <EditItemView - endpoint="notifications" - item={item} - setItem={setItem} - validate={validate} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'sharedNotification']} - > - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - value={item.type} - onChange={(e) => setItem({ ...item, type: e.target.value })} - endpoint="/api/notifications/types" - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('event', it.type))} - label={t('sharedType')} - /> - {item.type === 'alarm' && ( - <SelectField - multiple - value={item.attributes && item.attributes.alarms ? item.attributes.alarms.split(/[, ]+/) : []} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, alarms: e.target.value.join() } })} - data={alarms} - keyGetter={(it) => it.key} - label={t('sharedAlarms')} - /> - )} - <SelectField - multiple - value={item.notificators ? item.notificators.split(/[, ]+/) : []} - onChange={(e) => setItem({ ...item, notificators: e.target.value.join() })} - endpoint="/api/notifications/notificators" - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('notificator', it.type))} - label={t('notificationNotificators')} - /> - {item.notificators?.includes('command') && ( - <SelectField - value={item.commandId} - onChange={(event) => setItem({ ...item, commandId: Number(event.target.value) })} - endpoint="/api/commands" - titleGetter={(it) => it.description} - label={t('sharedSavedCommand')} - /> - )} - <Button - variant="outlined" - color="primary" - onClick={testNotificators} - disabled={!item.notificators} - > - {t('sharedTestNotificators')} - </Button> - <FormGroup> - <FormControlLabel - control={( - <Checkbox - checked={item.always} - onChange={(event) => setItem({ ...item, always: event.target.checked })} - /> - )} - label={t('notificationAlways')} - /> - </FormGroup> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedExtra')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - value={item.calendarId} - onChange={(event) => setItem({ ...item, calendarId: Number(event.target.value) })} - endpoint="/api/calendars" - label={t('sharedCalendar')} - /> - </AccordionDetails> - </Accordion> - </> - )} - </EditItemView> - ); -}; - -export default NotificationPage; diff --git a/modern/src/settings/NotificationsPage.jsx b/modern/src/settings/NotificationsPage.jsx deleted file mode 100644 index f1e70a85..00000000 --- a/modern/src/settings/NotificationsPage.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { prefixString } from '../common/util/stringUtils'; -import { formatBoolean } from '../common/util/formatter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const NotificationsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/notifications'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const formatList = (prefix, value) => { - if (value) { - return value - .split(/[, ]+/) - .filter(Boolean) - .map((it) => t(prefixString(prefix, it))) - .join(', '); - } - return ''; - }; - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedNotifications']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('notificationType')}</TableCell> - <TableCell>{t('notificationAlways')}</TableCell> - <TableCell>{t('sharedAlarms')}</TableCell> - <TableCell>{t('notificationNotificators')}</TableCell> - <TableCell className={classes.columnAction} /> - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{t(prefixString('event', item.type))}</TableCell> - <TableCell>{formatBoolean(item.always, t)}</TableCell> - <TableCell>{formatList('alarm', item.attributes.alarms)}</TableCell> - <TableCell>{formatList('notificator', item.notificators)}</TableCell> - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions itemId={item.id} editPath="/settings/notification" endpoint="notifications" setTimestamp={setTimestamp} /> - </TableCell> - </TableRow> - )) : (<TableShimmer columns={5} endAction />)} - </TableBody> - </Table> - <CollectionFab editPath="/settings/notification" /> - </PageLayout> - ); -}; - -export default NotificationsPage; diff --git a/modern/src/settings/PreferencesPage.jsx b/modern/src/settings/PreferencesPage.jsx deleted file mode 100644 index 2d6df62f..00000000 --- a/modern/src/settings/PreferencesPage.jsx +++ /dev/null @@ -1,375 +0,0 @@ -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 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'; -import useSettingsStyles from './common/useSettingsStyles'; - -const deviceFields = [ - { id: 'name', name: 'sharedName' }, - { id: 'uniqueId', name: 'deviceIdentifier' }, - { id: 'phone', name: 'sharedPhone' }, - { id: 'model', name: 'deviceModel' }, - { id: 'contact', name: 'deviceContact' }, -]; - -const PreferencesPage = () => { - const classes = useSettingsStyles(); - 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 ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedPreferences']}> - <Container maxWidth="xs" className={classes.container}> - {!readonly && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('mapTitle')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <FormControl> - <InputLabel>{t('mapActive')}</InputLabel> - <Select - label={t('mapActive')} - value={attributes.activeMapStyles?.split(',') || ['locationIqStreets', 'osm', 'carto']} - onChange={(e, child) => { - 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) => ( - <MenuItem key={style.id} value={style.id}> - <Typography component="span" color={style.available ? 'textPrimary' : 'error'}>{style.title}</Typography> - </MenuItem> - ))} - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('mapOverlay')}</InputLabel> - <Select - label={t('mapOverlay')} - value={attributes.selectedMapOverlay || ''} - onChange={(e) => { - 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()}`); - } - }} - > - <MenuItem value="">{'\u00a0'}</MenuItem> - {mapOverlays.map((overlay) => ( - <MenuItem key={overlay.id} value={overlay.id}> - <Typography component="span" color={overlay.available ? 'textPrimary' : 'error'}>{overlay.title}</Typography> - </MenuItem> - ))} - </Select> - </FormControl> - <Autocomplete - multiple - freeSolo - options={Object.keys(positionAttributes)} - getOptionLabel={(option) => (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) => ( - <TextField - {...params} - label={t('attributePopupInfo')} - /> - )} - /> - <FormControl> - <InputLabel>{t('mapLiveRoutes')}</InputLabel> - <Select - label={t('mapLiveRoutes')} - value={attributes.mapLiveRoutes || 'none'} - onChange={(e) => setAttributes({ ...attributes, mapLiveRoutes: e.target.value })} - > - <MenuItem value="none">{t('sharedDisabled')}</MenuItem> - <MenuItem value="selected">{t('deviceSelected')}</MenuItem> - <MenuItem value="all">{t('notificationAlways')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('mapDirection')}</InputLabel> - <Select - label={t('mapDirection')} - value={attributes.mapDirection || 'selected'} - onChange={(e) => setAttributes({ ...attributes, mapDirection: e.target.value })} - > - <MenuItem value="none">{t('sharedDisabled')}</MenuItem> - <MenuItem value="selected">{t('deviceSelected')}</MenuItem> - <MenuItem value="all">{t('notificationAlways')}</MenuItem> - </Select> - </FormControl> - <FormGroup> - <FormControlLabel - control={( - <Checkbox - checked={attributes.hasOwnProperty('mapGeofences') ? attributes.mapGeofences : true} - onChange={(e) => setAttributes({ ...attributes, mapGeofences: e.target.checked })} - /> - )} - label={t('attributeShowGeofences')} - /> - <FormControlLabel - control={( - <Checkbox - checked={attributes.hasOwnProperty('mapFollow') ? attributes.mapFollow : false} - onChange={(e) => setAttributes({ ...attributes, mapFollow: e.target.checked })} - /> - )} - label={t('deviceFollow')} - /> - <FormControlLabel - control={( - <Checkbox - checked={attributes.hasOwnProperty('mapCluster') ? attributes.mapCluster : true} - onChange={(e) => setAttributes({ ...attributes, mapCluster: e.target.checked })} - /> - )} - label={t('mapClustering')} - /> - <FormControlLabel - control={( - <Checkbox - checked={attributes.hasOwnProperty('mapOnSelect') ? attributes.mapOnSelect : true} - onChange={(e) => setAttributes({ ...attributes, mapOnSelect: e.target.checked })} - /> - )} - label={t('mapOnSelect')} - /> - </FormGroup> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('deviceTitle')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - value={attributes.devicePrimary || 'name'} - onChange={(e) => setAttributes({ ...attributes, devicePrimary: e.target.value })} - data={deviceFields} - titleGetter={(it) => t(it.name)} - label={t('devicePrimaryInfo')} - /> - <SelectField - value={attributes.deviceSecondary} - onChange={(e) => setAttributes({ ...attributes, deviceSecondary: e.target.value })} - data={deviceFields} - titleGetter={(it) => t(it.name)} - label={t('deviceSecondaryInfo')} - /> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedSound')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <SelectField - multiple - value={attributes.soundEvents?.split(',') || []} - onChange={(e) => 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')} - /> - <SelectField - multiple - value={attributes.soundAlarms?.split(',') || ['sos']} - onChange={(e) => setAttributes({ ...attributes, soundAlarms: e.target.value.join(',') })} - data={alarms} - keyGetter={(it) => it.key} - label={t('eventsSoundAlarms')} - /> - </AccordionDetails> - </Accordion> - </> - )} - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('userToken')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - label={t('userExpirationTime')} - type="date" - value={tokenExpiration} - onChange={(e) => { - setTokenExpiration(e.target.value); - setToken(null); - }} - /> - <FormControl> - <OutlinedInput - multiline - rows={6} - readOnly - type="text" - value={token || ''} - endAdornment={( - <InputAdornment position="end"> - <div className={classes.verticalActions}> - <IconButton size="small" edge="end" onClick={generateToken} disabled={!!token}> - <CachedIcon fontSize="small" /> - </IconButton> - <IconButton size="small" edge="end" onClick={() => navigator.clipboard.writeText(token)} disabled={!token}> - <ContentCopyIcon fontSize="small" /> - </IconButton> - </div> - </InputAdornment> - )} - /> - </FormControl> - </AccordionDetails> - </Accordion> - {!readonly && ( - <> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedInfoTitle')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={versionApp} - label={t('settingsAppVersion')} - disabled - /> - <TextField - value={versionServer || '-'} - label={t('settingsServerVersion')} - disabled - /> - <TextField - value={socket ? t('deviceStatusOnline') : t('deviceStatusOffline')} - label={t('settingsConnection')} - disabled - /> - </AccordionDetails> - </Accordion> - <div className={classes.buttons}> - <Button - type="button" - color="primary" - variant="outlined" - onClick={() => navigate(-1)} - > - {t('sharedCancel')} - </Button> - <Button - type="button" - color="primary" - variant="contained" - onClick={handleSave} - > - {t('sharedSave')} - </Button> - </div> - </> - )} - </Container> - </PageLayout> - ); -}; - -export default PreferencesPage; diff --git a/modern/src/settings/ServerPage.jsx b/modern/src/settings/ServerPage.jsx deleted file mode 100644 index 0ac76334..00000000 --- a/modern/src/settings/ServerPage.jsx +++ /dev/null @@ -1,316 +0,0 @@ -import React, { useState } from 'react'; -import TextField from '@mui/material/TextField'; - -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Button, - FormControl, - Container, - Checkbox, - FormControlLabel, - InputLabel, - Select, - MenuItem, - FormGroup, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useNavigate } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; -import { DropzoneArea } from 'react-mui-dropzone'; -import { sessionActions } from '../store'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SelectField from '../common/components/SelectField'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; -import useCommonUserAttributes from '../common/attributes/useCommonUserAttributes'; -import { useCatch } from '../reactHelper'; -import useServerAttributes from '../common/attributes/useServerAttributes'; -import useMapStyles from '../map/core/useMapStyles'; -import { map } from '../map/core/MapView'; -import useSettingsStyles from './common/useSettingsStyles'; - -const ServerPage = () => { - const classes = useSettingsStyles(); - const navigate = useNavigate(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const mapStyles = useMapStyles(); - const commonUserAttributes = useCommonUserAttributes(t); - const commonDeviceAttributes = useCommonDeviceAttributes(t); - const serverAttributes = useServerAttributes(t); - - const original = useSelector((state) => state.session.server); - const [item, setItem] = useState({ ...original }); - - const handleFiles = useCatch(async (files) => { - if (files.length > 0) { - const file = files[0]; - const response = await fetch(`/api/server/file/${file.path}`, { - method: 'POST', - body: file, - }); - if (!response.ok) { - throw Error(await response.text()); - } - } - }); - - const handleSave = useCatch(async () => { - const response = await fetch('/api/server', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - dispatch(sessionActions.updateServer(await response.json())); - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'settingsServer']}> - <Container maxWidth="xs" className={classes.container}> - {item && ( - <> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedPreferences')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.mapUrl || ''} - onChange={(event) => setItem({ ...item, mapUrl: event.target.value })} - label={t('mapCustomLabel')} - /> - <TextField - value={item.overlayUrl || ''} - onChange={(event) => setItem({ ...item, overlayUrl: event.target.value })} - label={t('mapOverlayCustom')} - /> - <FormControl> - <InputLabel>{t('mapDefault')}</InputLabel> - <Select - label={t('mapDefault')} - value={item.map || 'locationIqStreets'} - onChange={(e) => setItem({ ...item, map: e.target.value })} - > - {mapStyles.filter((style) => style.available).map((style) => ( - <MenuItem key={style.id} value={style.id}> - <Typography component="span">{style.title}</Typography> - </MenuItem> - ))} - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsCoordinateFormat')}</InputLabel> - <Select - label={t('settingsCoordinateFormat')} - value={item.coordinateFormat || 'dd'} - onChange={(event) => setItem({ ...item, coordinateFormat: event.target.value })} - > - <MenuItem value="dd">{t('sharedDecimalDegrees')}</MenuItem> - <MenuItem value="ddm">{t('sharedDegreesDecimalMinutes')}</MenuItem> - <MenuItem value="dms">{t('sharedDegreesMinutesSeconds')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsSpeedUnit')}</InputLabel> - <Select - label={t('settingsSpeedUnit')} - value={item.attributes.speedUnit || 'kn'} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, speedUnit: e.target.value } })} - > - <MenuItem value="kn">{t('sharedKn')}</MenuItem> - <MenuItem value="kmh">{t('sharedKmh')}</MenuItem> - <MenuItem value="mph">{t('sharedMph')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsDistanceUnit')}</InputLabel> - <Select - label={t('settingsDistanceUnit')} - value={item.attributes.distanceUnit || 'km'} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, distanceUnit: e.target.value } })} - > - <MenuItem value="km">{t('sharedKm')}</MenuItem> - <MenuItem value="mi">{t('sharedMi')}</MenuItem> - <MenuItem value="nmi">{t('sharedNmi')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsAltitudeUnit')}</InputLabel> - <Select - label={t('settingsAltitudeUnit')} - value={item.attributes.altitudeUnit || 'm'} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, altitudeUnit: e.target.value } })} - > - <MenuItem value="m">{t('sharedMeters')}</MenuItem> - <MenuItem value="ft">{t('sharedFeet')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsVolumeUnit')}</InputLabel> - <Select - label={t('settingsVolumeUnit')} - value={item.attributes.volumeUnit || 'ltr'} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, volumeUnit: e.target.value } })} - > - <MenuItem value="ltr">{t('sharedLiter')}</MenuItem> - <MenuItem value="usGal">{t('sharedUsGallon')}</MenuItem> - <MenuItem value="impGal">{t('sharedImpGallon')}</MenuItem> - </Select> - </FormControl> - <SelectField - value={item.attributes.timezone} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} - endpoint="/api/server/timezones" - keyGetter={(it) => it} - titleGetter={(it) => it} - label={t('sharedTimezone')} - /> - <TextField - value={item.poiLayer || ''} - onChange={(event) => setItem({ ...item, poiLayer: event.target.value })} - label={t('mapPoiLayer')} - /> - <TextField - value={item.announcement || ''} - onChange={(event) => setItem({ ...item, announcement: event.target.value })} - label={t('serverAnnouncement')} - /> - <FormGroup> - <FormControlLabel - control={<Checkbox checked={item.twelveHourFormat} onChange={(event) => setItem({ ...item, twelveHourFormat: event.target.checked })} />} - label={t('settingsTwelveHourFormat')} - /> - <FormControlLabel - control={<Checkbox checked={item.forceSettings} onChange={(event) => setItem({ ...item, forceSettings: event.target.checked })} />} - label={t('serverForceSettings')} - /> - </FormGroup> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedLocation')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - type="number" - value={item.latitude || 0} - onChange={(event) => setItem({ ...item, latitude: Number(event.target.value) })} - label={t('positionLatitude')} - /> - <TextField - type="number" - value={item.longitude || 0} - onChange={(event) => setItem({ ...item, longitude: Number(event.target.value) })} - label={t('positionLongitude')} - /> - <TextField - type="number" - value={item.zoom || 0} - onChange={(event) => setItem({ ...item, zoom: Number(event.target.value) })} - label={t('serverZoom')} - /> - <Button - variant="outlined" - color="primary" - onClick={() => { - const { lng, lat } = map.getCenter(); - setItem({ - ...item, - latitude: Number(lat.toFixed(6)), - longitude: Number(lng.toFixed(6)), - zoom: Number(map.getZoom().toFixed(1)), - }); - }} - > - {t('mapCurrentLocation')} - </Button> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedPermissions')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <FormGroup> - <FormControlLabel - control={<Checkbox checked={item.registration} onChange={(event) => setItem({ ...item, registration: event.target.checked })} />} - label={t('serverRegistration')} - /> - <FormControlLabel - control={<Checkbox checked={item.readonly} onChange={(event) => setItem({ ...item, readonly: event.target.checked })} />} - label={t('serverReadonly')} - /> - <FormControlLabel - control={<Checkbox checked={item.deviceReadonly} onChange={(event) => setItem({ ...item, deviceReadonly: event.target.checked })} />} - label={t('userDeviceReadonly')} - /> - <FormControlLabel - control={<Checkbox checked={item.limitCommands} onChange={(event) => setItem({ ...item, limitCommands: event.target.checked })} />} - label={t('userLimitCommands')} - /> - <FormControlLabel - control={<Checkbox checked={item.disableReports} onChange={(event) => setItem({ ...item, disableReports: event.target.checked })} />} - label={t('userDisableReports')} - /> - <FormControlLabel - control={<Checkbox checked={item.fixedEmail} onChange={(e) => setItem({ ...item, fixedEmail: e.target.checked })} />} - label={t('userFixedEmail')} - /> - </FormGroup> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedFile')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <DropzoneArea - dropzoneText={t('sharedDropzoneText')} - filesLimit={1} - onChange={handleFiles} - showAlerts={false} - /> - </AccordionDetails> - </Accordion> - <EditAttributesAccordion - attributes={item.attributes} - setAttributes={(attributes) => setItem({ ...item, attributes })} - definitions={{ ...commonUserAttributes, ...commonDeviceAttributes, ...serverAttributes }} - /> - </> - )} - <div className={classes.buttons}> - <Button type="button" color="primary" variant="outlined" onClick={() => navigate(-1)}> - {t('sharedCancel')} - </Button> - <Button type="button" color="primary" variant="contained" onClick={handleSave}> - {t('sharedSave')} - </Button> - </div> - </Container> - </PageLayout> - ); -}; - -export default ServerPage; diff --git a/modern/src/settings/SharePage.jsx b/modern/src/settings/SharePage.jsx deleted file mode 100644 index d16fe44d..00000000 --- a/modern/src/settings/SharePage.jsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate, useParams } from 'react-router-dom'; -import dayjs from 'dayjs'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, - TextField, - Button, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatchCallback } from '../reactHelper'; -import useSettingsStyles from './common/useSettingsStyles'; - -const SharePage = () => { - const navigate = useNavigate(); - const classes = useSettingsStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const device = useSelector((state) => state.devices.items[id]); - - const [expiration, setExpiration] = useState(dayjs().add(1, 'week').locale('en').format('YYYY-MM-DD')); - const [link, setLink] = useState(); - - const handleShare = useCatchCallback(async () => { - const expirationTime = dayjs(expiration).toISOString(); - const response = await fetch('/api/devices/share', { - method: 'POST', - body: new URLSearchParams(`deviceId=${id}&expiration=${expirationTime}`), - }); - if (response.ok) { - const token = await response.text(); - setLink(`${window.location.origin}?token=${token}`); - } else { - throw Error(await response.text()); - } - }, [id, expiration, setLink]); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['deviceShare']}> - <Container maxWidth="xs" className={classes.container}> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={device.name} - label={t('sharedDevice')} - disabled - /> - <TextField - label={t('userExpirationTime')} - type="date" - value={(expiration && dayjs(expiration).locale('en').format('YYYY-MM-DD')) || '2099-01-01'} - onChange={(e) => setExpiration(dayjs(e.target.value, 'YYYY-MM-DD').locale('en').format())} - /> - <Button - variant="outlined" - color="primary" - onClick={handleShare} - > - {t('reportShow')} - </Button> - <TextField - value={link || ''} - onChange={(e) => setLink(e.target.value)} - label={t('sharedLink')} - InputProps={{ - readOnly: true, - }} - /> - </AccordionDetails> - </Accordion> - <div className={classes.buttons}> - <Button - type="button" - color="primary" - variant="outlined" - onClick={() => navigate(-1)} - > - {t('sharedCancel')} - </Button> - <Button - type="button" - color="primary" - variant="contained" - onClick={() => navigator.clipboard?.writeText(link)} - disabled={!link} - > - {t('sharedCopy')} - </Button> - </div> - </Container> - </PageLayout> - ); -}; - -export default SharePage; diff --git a/modern/src/settings/UserConnectionsPage.jsx b/modern/src/settings/UserConnectionsPage.jsx deleted file mode 100644 index 3ca0bdc1..00000000 --- a/modern/src/settings/UserConnectionsPage.jsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import { useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import LinkField from '../common/components/LinkField'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import { formatNotificationTitle } from '../common/util/formatter'; -import PageLayout from '../common/components/PageLayout'; -import useSettingsStyles from './common/useSettingsStyles'; - -const UserConnectionsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - return ( - <PageLayout - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'settingsUser', 'sharedConnections']} - > - <Container maxWidth="xs" className={classes.container}> - <Accordion defaultExpanded> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedConnections')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <LinkField - endpointAll="/api/devices?all=true" - endpointLinked={`/api/devices?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="deviceId" - titleGetter={(it) => `${it.name} (${it.uniqueId})`} - label={t('deviceTitle')} - /> - <LinkField - endpointAll="/api/groups?all=true" - endpointLinked={`/api/groups?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="groupId" - label={t('settingsGroups')} - /> - <LinkField - endpointAll="/api/geofences?all=true" - endpointLinked={`/api/geofences?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="geofenceId" - label={t('sharedGeofences')} - /> - <LinkField - endpointAll="/api/notifications?all=true" - endpointLinked={`/api/notifications?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="notificationId" - titleGetter={(it) => formatNotificationTitle(t, it, true)} - label={t('sharedNotifications')} - /> - <LinkField - endpointAll="/api/calendars?all=true" - endpointLinked={`/api/calendars?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="calendarId" - label={t('sharedCalendars')} - /> - <LinkField - endpointAll="/api/users?all=true" - endpointLinked={`/api/users?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="managedUserId" - label={t('settingsUsers')} - /> - <LinkField - endpointAll="/api/attributes/computed?all=true" - endpointLinked={`/api/attributes/computed?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="attributeId" - titleGetter={(it) => it.description} - label={t('sharedComputedAttributes')} - /> - <LinkField - endpointAll="/api/drivers?all=true" - endpointLinked={`/api/drivers?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="driverId" - titleGetter={(it) => `${it.name} (${it.uniqueId})`} - label={t('sharedDrivers')} - /> - <LinkField - endpointAll="/api/commands?all=true" - endpointLinked={`/api/commands?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="commandId" - titleGetter={(it) => it.description} - label={t('sharedSavedCommands')} - /> - <LinkField - endpointAll="/api/maintenance?all=true" - endpointLinked={`/api/maintenance?userId=${id}`} - baseId={id} - keyBase="userId" - keyLink="maintenanceId" - label={t('sharedMaintenance')} - /> - </AccordionDetails> - </Accordion> - </Container> - </PageLayout> - ); -}; - -export default UserConnectionsPage; diff --git a/modern/src/settings/UserPage.jsx b/modern/src/settings/UserPage.jsx deleted file mode 100644 index 6748dd31..00000000 --- a/modern/src/settings/UserPage.jsx +++ /dev/null @@ -1,428 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - FormControl, - InputLabel, - Select, - MenuItem, - FormControlLabel, - Checkbox, - FormGroup, - TextField, - Button, - InputAdornment, - IconButton, - OutlinedInput, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; -import CachedIcon from '@mui/icons-material/Cached'; -import CloseIcon from '@mui/icons-material/Close'; -import { useDispatch, useSelector } from 'react-redux'; -import dayjs from 'dayjs'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import useUserAttributes from '../common/attributes/useUserAttributes'; -import { sessionActions } from '../store'; -import SelectField from '../common/components/SelectField'; -import SettingsMenu from './components/SettingsMenu'; -import useCommonUserAttributes from '../common/attributes/useCommonUserAttributes'; -import { useAdministrator, useRestriction, useManager } from '../common/util/permissions'; -import useQuery from '../common/util/useQuery'; -import { useCatch } from '../reactHelper'; -import useMapStyles from '../map/core/useMapStyles'; -import { map } from '../map/core/MapView'; -import useSettingsStyles from './common/useSettingsStyles'; - -const UserPage = () => { - const classes = useSettingsStyles(); - const navigate = useNavigate(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const admin = useAdministrator(); - const manager = useManager(); - const fixedEmail = useRestriction('fixedEmail'); - - const currentUser = useSelector((state) => state.session.user); - const registrationEnabled = useSelector((state) => state.session.server.registration); - const openIdForced = useSelector((state) => state.session.server.openIdForce); - const totpEnable = useSelector((state) => state.session.server.attributes.totpEnable); - const totpForce = useSelector((state) => state.session.server.attributes.totpForce); - - const mapStyles = useMapStyles(); - const commonUserAttributes = useCommonUserAttributes(t); - const userAttributes = useUserAttributes(t); - - const { id } = useParams(); - const [item, setItem] = useState(id === currentUser.id.toString() ? currentUser : null); - - const [deleteEmail, setDeleteEmail] = useState(); - const [deleteFailed, setDeleteFailed] = useState(false); - - const handleDelete = useCatch(async () => { - if (deleteEmail === currentUser.email) { - setDeleteFailed(false); - const response = await fetch(`/api/users/${currentUser.id}`, { method: 'DELETE' }); - if (response.ok) { - navigate('/login'); - dispatch(sessionActions.updateUser(null)); - } else { - throw Error(await response.text()); - } - } else { - setDeleteFailed(true); - } - }); - - const handleGenerateTotp = useCatch(async () => { - const response = await fetch('/api/users/totp', { method: 'POST' }); - if (response.ok) { - setItem({ ...item, totpKey: await response.text() }); - } else { - throw Error(await response.text()); - } - }); - - const query = useQuery(); - const [queryHandled, setQueryHandled] = useState(false); - const attribute = query.get('attribute'); - - useEffect(() => { - if (!queryHandled && item && attribute) { - if (!item.attributes.hasOwnProperty('attribute')) { - const updatedAttributes = { ...item.attributes }; - updatedAttributes[attribute] = ''; - setItem({ ...item, attributes: updatedAttributes }); - } - setQueryHandled(true); - } - }, [item, queryHandled, setQueryHandled, attribute]); - - const onItemSaved = (result) => { - if (result.id === currentUser.id) { - dispatch(sessionActions.updateUser(result)); - } - }; - - const validate = () => item && item.name && item.email && (item.id || item.password) && (admin || !totpForce || item.totpKey); - - return ( - <EditItemView - endpoint="users" - item={item} - setItem={setItem} - defaultItem={admin ? { deviceLimit: -1 } : {}} - validate={validate} - onItemSaved={onItemSaved} - menu={<SettingsMenu />} - breadcrumbs={['settingsTitle', 'settingsUser']} - > - {item && ( - <> - <Accordion defaultExpanded={!attribute}> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedRequired')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.name || ''} - onChange={(e) => setItem({ ...item, name: e.target.value })} - label={t('sharedName')} - /> - <TextField - value={item.email || ''} - onChange={(e) => setItem({ ...item, email: e.target.value })} - label={t('userEmail')} - disabled={fixedEmail} - /> - {!openIdForced && ( - <TextField - type="password" - onChange={(e) => setItem({ ...item, password: e.target.value })} - label={t('userPassword')} - /> - )} - {totpEnable && ( - <FormControl> - <InputLabel>{t('loginTotpKey')}</InputLabel> - <OutlinedInput - readOnly - label={t('loginTotpKey')} - value={item.totpKey || ''} - endAdornment={( - <InputAdornment position="end"> - <IconButton size="small" edge="end" onClick={handleGenerateTotp}> - <CachedIcon fontSize="small" /> - </IconButton> - <IconButton size="small" edge="end" onClick={() => setItem({ ...item, totpKey: null })}> - <CloseIcon fontSize="small" /> - </IconButton> - </InputAdornment> - )} - /> - </FormControl> - )} - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedPreferences')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={item.phone || ''} - onChange={(e) => setItem({ ...item, phone: e.target.value })} - label={t('sharedPhone')} - /> - <FormControl> - <InputLabel>{t('mapDefault')}</InputLabel> - <Select - label={t('mapDefault')} - value={item.map || 'locationIqStreets'} - onChange={(e) => setItem({ ...item, map: e.target.value })} - > - {mapStyles.filter((style) => style.available).map((style) => ( - <MenuItem key={style.id} value={style.id}> - <Typography component="span">{style.title}</Typography> - </MenuItem> - ))} - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsCoordinateFormat')}</InputLabel> - <Select - label={t('settingsCoordinateFormat')} - value={item.coordinateFormat || 'dd'} - onChange={(e) => setItem({ ...item, coordinateFormat: e.target.value })} - > - <MenuItem value="dd">{t('sharedDecimalDegrees')}</MenuItem> - <MenuItem value="ddm">{t('sharedDegreesDecimalMinutes')}</MenuItem> - <MenuItem value="dms">{t('sharedDegreesMinutesSeconds')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsSpeedUnit')}</InputLabel> - <Select - label={t('settingsSpeedUnit')} - value={(item.attributes && item.attributes.speedUnit) || 'kn'} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, speedUnit: e.target.value } })} - > - <MenuItem value="kn">{t('sharedKn')}</MenuItem> - <MenuItem value="kmh">{t('sharedKmh')}</MenuItem> - <MenuItem value="mph">{t('sharedMph')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsDistanceUnit')}</InputLabel> - <Select - label={t('settingsDistanceUnit')} - value={(item.attributes && item.attributes.distanceUnit) || 'km'} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, distanceUnit: e.target.value } })} - > - <MenuItem value="km">{t('sharedKm')}</MenuItem> - <MenuItem value="mi">{t('sharedMi')}</MenuItem> - <MenuItem value="nmi">{t('sharedNmi')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsAltitudeUnit')}</InputLabel> - <Select - label={t('settingsAltitudeUnit')} - value={(item.attributes && item.attributes.altitudeUnit) || 'm'} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, altitudeUnit: e.target.value } })} - > - <MenuItem value="m">{t('sharedMeters')}</MenuItem> - <MenuItem value="ft">{t('sharedFeet')}</MenuItem> - </Select> - </FormControl> - <FormControl> - <InputLabel>{t('settingsVolumeUnit')}</InputLabel> - <Select - label={t('settingsVolumeUnit')} - value={(item.attributes && item.attributes.volumeUnit) || 'ltr'} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, volumeUnit: e.target.value } })} - > - <MenuItem value="ltr">{t('sharedLiter')}</MenuItem> - <MenuItem value="usGal">{t('sharedUsGallon')}</MenuItem> - <MenuItem value="impGal">{t('sharedImpGallon')}</MenuItem> - </Select> - </FormControl> - <SelectField - value={item.attributes && item.attributes.timezone} - onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} - endpoint="/api/server/timezones" - keyGetter={(it) => it} - titleGetter={(it) => it} - label={t('sharedTimezone')} - /> - <TextField - value={item.poiLayer || ''} - onChange={(e) => setItem({ ...item, poiLayer: e.target.value })} - label={t('mapPoiLayer')} - /> - <FormGroup> - <FormControlLabel - control={<Checkbox checked={item.twelveHourFormat} onChange={(e) => setItem({ ...item, twelveHourFormat: e.target.checked })} />} - label={t('settingsTwelveHourFormat')} - /> - </FormGroup> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedLocation')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - type="number" - value={item.latitude || 0} - onChange={(e) => setItem({ ...item, latitude: Number(e.target.value) })} - label={t('positionLatitude')} - /> - <TextField - type="number" - value={item.longitude || 0} - onChange={(e) => setItem({ ...item, longitude: Number(e.target.value) })} - label={t('positionLongitude')} - /> - <TextField - type="number" - value={item.zoom || 0} - onChange={(e) => setItem({ ...item, zoom: Number(e.target.value) })} - label={t('serverZoom')} - /> - <Button - variant="outlined" - color="primary" - onClick={() => { - const { lng, lat } = map.getCenter(); - setItem({ - ...item, - latitude: Number(lat.toFixed(6)), - longitude: Number(lng.toFixed(6)), - zoom: Number(map.getZoom().toFixed(1)), - }); - }} - > - {t('mapCurrentLocation')} - </Button> - </AccordionDetails> - </Accordion> - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedPermissions')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - label={t('userExpirationTime')} - type="date" - value={(item.expirationTime && dayjs(item.expirationTime).locale('en').format('YYYY-MM-DD')) || '2099-01-01'} - onChange={(e) => setItem({ ...item, expirationTime: dayjs(e.target.value, 'YYYY-MM-DD').locale('en').format() })} - disabled={!manager} - /> - <TextField - type="number" - value={item.deviceLimit || 0} - onChange={(e) => setItem({ ...item, deviceLimit: Number(e.target.value) })} - label={t('userDeviceLimit')} - disabled={!admin} - /> - <TextField - type="number" - value={item.userLimit || 0} - onChange={(e) => setItem({ ...item, userLimit: Number(e.target.value) })} - label={t('userUserLimit')} - disabled={!admin} - /> - <FormGroup> - <FormControlLabel - control={<Checkbox checked={item.disabled} onChange={(e) => setItem({ ...item, disabled: e.target.checked })} />} - label={t('sharedDisabled')} - disabled={!manager} - /> - <FormControlLabel - control={<Checkbox checked={item.administrator} onChange={(e) => setItem({ ...item, administrator: e.target.checked })} />} - label={t('userAdmin')} - disabled={!admin} - /> - <FormControlLabel - control={<Checkbox checked={item.readonly} onChange={(e) => setItem({ ...item, readonly: e.target.checked })} />} - label={t('serverReadonly')} - disabled={!manager} - /> - <FormControlLabel - control={<Checkbox checked={item.deviceReadonly} onChange={(e) => setItem({ ...item, deviceReadonly: e.target.checked })} />} - label={t('userDeviceReadonly')} - disabled={!manager} - /> - <FormControlLabel - control={<Checkbox checked={item.limitCommands} onChange={(e) => setItem({ ...item, limitCommands: e.target.checked })} />} - label={t('userLimitCommands')} - disabled={!manager} - /> - <FormControlLabel - control={<Checkbox checked={item.disableReports} onChange={(e) => setItem({ ...item, disableReports: e.target.checked })} />} - label={t('userDisableReports')} - disabled={!manager} - /> - <FormControlLabel - control={<Checkbox checked={item.fixedEmail} onChange={(e) => setItem({ ...item, fixedEmail: e.target.checked })} />} - label={t('userFixedEmail')} - disabled={!manager} - /> - </FormGroup> - </AccordionDetails> - </Accordion> - <EditAttributesAccordion - attribute={attribute} - attributes={item.attributes} - setAttributes={(attributes) => setItem({ ...item, attributes })} - definitions={{ ...commonUserAttributes, ...userAttributes }} - focusAttribute={attribute} - /> - {registrationEnabled && item.id === currentUser.id && !manager && ( - <Accordion> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1" color="error"> - {t('userDeleteAccount')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - <TextField - value={deleteEmail} - onChange={(e) => setDeleteEmail(e.target.value)} - label={t('userEmail')} - error={deleteFailed} - /> - <Button - variant="outlined" - color="error" - onClick={handleDelete} - startIcon={<DeleteForeverIcon />} - > - {t('userDeleteAccount')} - </Button> - </AccordionDetails> - </Accordion> - )} - </> - )} - </EditItemView> - ); -}; - -export default UserPage; diff --git a/modern/src/settings/UsersPage.jsx b/modern/src/settings/UsersPage.jsx deleted file mode 100644 index 2941965b..00000000 --- a/modern/src/settings/UsersPage.jsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Table, TableRow, TableCell, TableHead, TableBody, Switch, TableFooter, FormControlLabel, -} from '@mui/material'; -import LoginIcon from '@mui/icons-material/Login'; -import LinkIcon from '@mui/icons-material/Link'; -import { useCatch, useEffectAsync } from '../reactHelper'; -import { formatBoolean, formatTime } from '../common/util/formatter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import { useManager } from '../common/util/permissions'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import { usePreference } from '../common/util/preferences'; -import useSettingsStyles from './common/useSettingsStyles'; - -const UsersPage = () => { - const classes = useSettingsStyles(); - const navigate = useNavigate(); - const t = useTranslation(); - - const manager = useManager(); - - const hours12 = usePreference('twelveHourFormat'); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - const [temporary, setTemporary] = useState(false); - - const handleLogin = useCatch(async (userId) => { - const response = await fetch(`/api/session/${userId}`); - if (response.ok) { - window.location.replace('/'); - } else { - throw Error(await response.text()); - } - }); - - const actionLogin = { - key: 'login', - title: t('loginLogin'), - icon: <LoginIcon fontSize="small" />, - handler: handleLogin, - }; - - const actionConnections = { - key: 'connections', - title: t('sharedConnections'), - icon: <LinkIcon fontSize="small" />, - handler: (userId) => navigate(`/settings/user/${userId}/connections`), - }; - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/users'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'settingsUsers']}> - <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} /> - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell>{t('sharedName')}</TableCell> - <TableCell>{t('userEmail')}</TableCell> - <TableCell>{t('userAdmin')}</TableCell> - <TableCell>{t('sharedDisabled')}</TableCell> - <TableCell>{t('userExpirationTime')}</TableCell> - <TableCell className={classes.columnAction} /> - </TableRow> - </TableHead> - <TableBody> - {!loading ? items.filter((u) => temporary || !u.temporary).filter(filterByKeyword(searchKeyword)).map((item) => ( - <TableRow key={item.id}> - <TableCell>{item.name}</TableCell> - <TableCell>{item.email}</TableCell> - <TableCell>{formatBoolean(item.administrator, t)}</TableCell> - <TableCell>{formatBoolean(item.disabled, t)}</TableCell> - <TableCell>{formatTime(item.expirationTime, 'date', hours12)}</TableCell> - <TableCell className={classes.columnAction} padding="none"> - <CollectionActions - itemId={item.id} - editPath="/settings/user" - endpoint="users" - setTimestamp={setTimestamp} - customActions={manager ? [actionLogin, actionConnections] : [actionConnections]} - /> - </TableCell> - </TableRow> - )) : (<TableShimmer columns={6} endAction />)} - </TableBody> - <TableFooter> - <TableRow> - <TableCell colSpan={6} align="right"> - <FormControlLabel - control={( - <Switch - value={temporary} - onChange={(e) => setTemporary(e.target.checked)} - size="small" - /> - )} - label={t('userTemporary')} - labelPlacement="start" - /> - </TableCell> - </TableRow> - </TableFooter> - </Table> - <CollectionFab editPath="/settings/user" /> - </PageLayout> - ); -}; - -export default UsersPage; diff --git a/modern/src/settings/common/useSettingsStyles.js b/modern/src/settings/common/useSettingsStyles.js deleted file mode 100644 index b276e0b7..00000000 --- a/modern/src/settings/common/useSettingsStyles.js +++ /dev/null @@ -1,33 +0,0 @@ -import { makeStyles } from '@mui/styles'; - -export default makeStyles((theme) => ({ - table: { - marginBottom: theme.spacing(10), - }, - columnAction: { - width: '1%', - paddingRight: theme.spacing(1), - }, - 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), - }, - verticalActions: { - display: 'flex', - flexDirection: 'column', - }, -})); diff --git a/modern/src/settings/components/AddAttributeDialog.jsx b/modern/src/settings/components/AddAttributeDialog.jsx deleted file mode 100644 index 86ff64ea..00000000 --- a/modern/src/settings/components/AddAttributeDialog.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import { - Button, Dialog, DialogActions, DialogContent, FormControl, InputLabel, MenuItem, Select, TextField, Autocomplete, -} from '@mui/material'; - -import { createFilterOptions } from '@mui/material/useAutocomplete'; -import { makeStyles } from '@mui/styles'; -import { useTranslation } from '../../common/components/LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(1), - paddingTop: theme.spacing(3), - }, -})); - -const AddAttributeDialog = ({ open, onResult, definitions }) => { - const classes = useStyles(); - const t = useTranslation(); - - const filter = createFilterOptions({ - stringify: (option) => option.name, - }); - - const options = Object.entries(definitions).map(([key, value]) => ({ - key, - name: value.name, - type: value.type, - })); - - const [key, setKey] = useState(); - const [type, setType] = useState('string'); - - return ( - <Dialog open={open} fullWidth maxWidth="xs"> - <DialogContent className={classes.details}> - <Autocomplete - onChange={(_, option) => { - setKey(option && typeof option === 'object' ? option.key : option); - if (option && option.type) { - setType(option.type); - } - }} - filterOptions={(options, params) => { - const filtered = filter(options, params); - if (params.inputValue) { - filtered.push({ - key: params.inputValue, - name: params.inputValue, - }); - } - return filtered; - }} - options={options} - getOptionLabel={(option) => (option && typeof option === 'object' ? option.name : option)} - renderOption={(props, option) => ( - <li {...props}> - {option.name} - </li> - )} - renderInput={(params) => ( - <TextField {...params} label={t('sharedAttribute')} /> - )} - freeSolo - /> - <FormControl - fullWidth - disabled={key in definitions} - > - <InputLabel>{t('sharedType')}</InputLabel> - <Select - label={t('sharedType')} - value={type} - onChange={(e) => setType(e.target.value)} - > - <MenuItem value="string">{t('sharedTypeString')}</MenuItem> - <MenuItem value="number">{t('sharedTypeNumber')}</MenuItem> - <MenuItem value="boolean">{t('sharedTypeBoolean')}</MenuItem> - </Select> - </FormControl> - </DialogContent> - <DialogActions> - <Button - color="primary" - disabled={!key} - onClick={() => onResult({ key, type })} - > - {t('sharedAdd')} - </Button> - <Button - autoFocus - onClick={() => onResult(null)} - > - {t('sharedCancel')} - </Button> - </DialogActions> - </Dialog> - ); -}; - -export default AddAttributeDialog; diff --git a/modern/src/settings/components/BaseCommandView.jsx b/modern/src/settings/components/BaseCommandView.jsx deleted file mode 100644 index bb70c3b9..00000000 --- a/modern/src/settings/components/BaseCommandView.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { - TextField, FormControlLabel, Checkbox, -} from '@mui/material'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import SelectField from '../../common/components/SelectField'; -import { prefixString } from '../../common/util/stringUtils'; -import useCommandAttributes from '../../common/attributes/useCommandAttributes'; - -const BaseCommandView = ({ deviceId, item, setItem }) => { - const t = useTranslation(); - - const textEnabled = useSelector((state) => state.session.server.textEnabled); - - const availableAttributes = useCommandAttributes(t); - - const [attributes, setAttributes] = useState([]); - - useEffect(() => { - if (item && item.type) { - setAttributes(availableAttributes[item.type] || []); - } else { - setAttributes([]); - } - }, [availableAttributes, item]); - - return ( - <> - <SelectField - value={item.type} - onChange={(e) => setItem({ ...item, type: e.target.value, attributes: {} })} - endpoint={deviceId ? `/api/commands/types?${new URLSearchParams({ deviceId }).toString()}` : '/api/commands/types'} - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('command', it.type))} - label={t('sharedType')} - /> - {attributes.map(({ key, name, type }) => { - if (type === 'boolean') { - return ( - <FormControlLabel - control={( - <Checkbox - checked={item.attributes[key]} - onChange={(e) => { - const updateItem = { ...item, attributes: { ...item.attributes } }; - updateItem.attributes[key] = e.target.checked; - setItem(updateItem); - }} - /> - )} - label={name} - /> - ); - } - return ( - <TextField - type={type === 'number' ? 'number' : 'text'} - value={item.attributes[key]} - onChange={(e) => { - const updateItem = { ...item, attributes: { ...item.attributes } }; - updateItem.attributes[key] = type === 'number' ? Number(e.target.value) : e.target.value; - setItem(updateItem); - }} - label={name} - /> - ); - })} - {textEnabled && ( - <FormControlLabel - control={<Checkbox checked={item.textChannel} onChange={(event) => setItem({ ...item, textChannel: event.target.checked })} />} - label={t('commandSendSms')} - /> - )} - </> - ); -}; - -export default BaseCommandView; diff --git a/modern/src/settings/components/CollectionActions.jsx b/modern/src/settings/components/CollectionActions.jsx deleted file mode 100644 index 666052d5..00000000 --- a/modern/src/settings/components/CollectionActions.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import { - IconButton, Menu, MenuItem, useMediaQuery, useTheme, -} from '@mui/material'; -import Tooltip from '@mui/material/Tooltip'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import EditIcon from '@mui/icons-material/Edit'; -import DeleteIcon from '@mui/icons-material/Delete'; -import { useNavigate } from 'react-router-dom'; -import { makeStyles } from '@mui/styles'; -import RemoveDialog from '../../common/components/RemoveDialog'; -import { useTranslation } from '../../common/components/LocalizationProvider'; - -const useStyles = makeStyles(() => ({ - row: { - display: 'flex', - }, -})); - -const CollectionActions = ({ - itemId, editPath, endpoint, setTimestamp, customActions, readonly, -}) => { - const theme = useTheme(); - const classes = useStyles(); - const navigate = useNavigate(); - const t = useTranslation(); - - const phone = useMediaQuery(theme.breakpoints.down('sm')); - - const [menuAnchorEl, setMenuAnchorEl] = useState(null); - const [removing, setRemoving] = useState(false); - - const handleEdit = () => { - navigate(`${editPath}/${itemId}`); - setMenuAnchorEl(null); - }; - - const handleRemove = () => { - setRemoving(true); - setMenuAnchorEl(null); - }; - - const handleCustom = (action) => { - action.handler(itemId); - setMenuAnchorEl(null); - }; - - const handleRemoveResult = (removed) => { - setRemoving(false); - if (removed) { - setTimestamp(Date.now()); - } - }; - - return ( - <> - {phone ? ( - <> - <IconButton size="small" onClick={(event) => setMenuAnchorEl(event.currentTarget)}> - <MoreVertIcon fontSize="small" /> - </IconButton> - <Menu open={!!menuAnchorEl} anchorEl={menuAnchorEl} onClose={() => setMenuAnchorEl(null)}> - {customActions && customActions.map((action) => ( - <MenuItem onClick={() => handleCustom(action)} key={action.key}>{action.title}</MenuItem> - ))} - {!readonly && ( - <> - <MenuItem onClick={handleEdit}>{t('sharedEdit')}</MenuItem> - <MenuItem onClick={handleRemove}>{t('sharedRemove')}</MenuItem> - </> - )} - </Menu> - </> - ) : ( - <div className={classes.row}> - {customActions && customActions.map((action) => ( - <Tooltip title={action.title} key={action.key}> - <IconButton size="small" onClick={() => handleCustom(action)}> - {action.icon} - </IconButton> - </Tooltip> - ))} - {!readonly && ( - <> - <Tooltip title={t('sharedEdit')}> - <IconButton size="small" onClick={handleEdit}> - <EditIcon fontSize="small" /> - </IconButton> - </Tooltip> - <Tooltip title={t('sharedRemove')}> - <IconButton size="small" onClick={handleRemove}> - <DeleteIcon fontSize="small" /> - </IconButton> - </Tooltip> - </> - )} - </div> - )} - <RemoveDialog style={{ transform: 'none' }} open={removing} endpoint={endpoint} itemId={itemId} onResult={handleRemoveResult} /> - </> - ); -}; - -export default CollectionActions; diff --git a/modern/src/settings/components/CollectionFab.jsx b/modern/src/settings/components/CollectionFab.jsx deleted file mode 100644 index 3c1fa783..00000000 --- a/modern/src/settings/components/CollectionFab.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { Fab } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import AddIcon from '@mui/icons-material/Add'; -import { useNavigate } from 'react-router-dom'; -import { useRestriction } from '../../common/util/permissions'; - -const useStyles = makeStyles((theme) => ({ - fab: { - position: 'fixed', - bottom: theme.spacing(2), - right: theme.spacing(2), - [theme.breakpoints.down('md')]: { - bottom: `calc(${theme.dimensions.bottomBarHeight}px + ${theme.spacing(2)})`, - }, - }, -})); - -const CollectionFab = ({ editPath, disabled }) => { - const classes = useStyles(); - const navigate = useNavigate(); - - const readonly = useRestriction('readonly'); - - if (!readonly && !disabled) { - return ( - <Fab size="medium" color="primary" className={classes.fab} onClick={() => navigate(editPath)}> - <AddIcon /> - </Fab> - ); - } - return ''; -}; - -export default CollectionFab; diff --git a/modern/src/settings/components/EditAttributesAccordion.jsx b/modern/src/settings/components/EditAttributesAccordion.jsx deleted file mode 100644 index 4d4ae254..00000000 --- a/modern/src/settings/components/EditAttributesAccordion.jsx +++ /dev/null @@ -1,217 +0,0 @@ -import React, { useState } from 'react'; - -import { - Button, - Checkbox, - OutlinedInput, - FormControl, - FormControlLabel, - Grid, - IconButton, - InputAdornment, - InputLabel, - Accordion, - AccordionSummary, - Typography, - AccordionDetails, -} from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import AddIcon from '@mui/icons-material/Add'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import AddAttributeDialog from './AddAttributeDialog'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import { useAttributePreference } from '../../common/util/preferences'; -import { - distanceFromMeters, distanceToMeters, distanceUnitString, speedFromKnots, speedToKnots, speedUnitString, volumeFromLiters, volumeToLiters, volumeUnitString, -} from '../../common/util/converter'; -import useFeatures from '../../common/util/useFeatures'; -import useSettingsStyles from '../common/useSettingsStyles'; - -const EditAttributesAccordion = ({ attribute, attributes, setAttributes, definitions, focusAttribute }) => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const features = useFeatures(); - - const speedUnit = useAttributePreference('speedUnit'); - const distanceUnit = useAttributePreference('distanceUnit'); - const volumeUnit = useAttributePreference('volumeUnit'); - - const [addDialogShown, setAddDialogShown] = useState(false); - - const updateAttribute = (key, value, type, subtype) => { - const updatedAttributes = { ...attributes }; - switch (subtype) { - case 'speed': - updatedAttributes[key] = speedToKnots(Number(value), speedUnit); - break; - case 'distance': - updatedAttributes[key] = distanceToMeters(Number(value), distanceUnit); - break; - case 'volume': - updatedAttributes[key] = volumeToLiters(Number(value), volumeUnit); - break; - default: - updatedAttributes[key] = type === 'number' ? Number(value) : value; - break; - } - setAttributes(updatedAttributes); - }; - - const deleteAttribute = (key) => { - const updatedAttributes = { ...attributes }; - delete updatedAttributes[key]; - setAttributes(updatedAttributes); - }; - - const getAttributeName = (key, subtype) => { - const definition = definitions[key]; - const name = definition ? definition.name : key; - switch (subtype) { - case 'speed': - return `${name} (${speedUnitString(speedUnit, t)})`; - case 'distance': - return `${name} (${distanceUnitString(distanceUnit, t)})`; - case 'volume': - return `${name} (${volumeUnitString(volumeUnit, t)})`; - default: - return name; - } - }; - - const getAttributeType = (value) => { - if (typeof value === 'number') { - return 'number'; - } if (typeof value === 'boolean') { - return 'boolean'; - } - return 'string'; - }; - - const getAttributeSubtype = (key) => { - const definition = definitions[key]; - return definition && definition.subtype; - }; - - const getDisplayValue = (value, subtype) => { - if (value) { - switch (subtype) { - case 'speed': - return speedFromKnots(value, speedUnit); - case 'distance': - return distanceFromMeters(value, distanceUnit); - case 'volume': - return volumeFromLiters(value, volumeUnit); - default: - return value; - } - } - return ''; - }; - - const convertToList = (attributes) => { - const booleanList = []; - const otherList = []; - const excludeAttributes = ['speedUnit', 'distanceUnit', 'volumeUnit', 'timezone']; - Object.keys(attributes || []).filter((key) => !excludeAttributes.includes(key)).forEach((key) => { - const value = attributes[key]; - const type = getAttributeType(value); - const subtype = getAttributeSubtype(key); - if (type === 'boolean') { - booleanList.push({ - key, value, type, subtype, - }); - } else { - otherList.push({ - key, value, type, subtype, - }); - } - }); - return [...otherList, ...booleanList]; - }; - - const handleAddResult = (definition) => { - setAddDialogShown(false); - if (definition) { - switch (definition.type) { - case 'number': - updateAttribute(definition.key, 0); - break; - case 'boolean': - updateAttribute(definition.key, false); - break; - default: - updateAttribute(definition.key, ''); - break; - } - } - }; - - return features.disableAttributes ? '' : ( - <Accordion defaultExpanded={!!attribute}> - <AccordionSummary expandIcon={<ExpandMoreIcon />}> - <Typography variant="subtitle1"> - {t('sharedAttributes')} - </Typography> - </AccordionSummary> - <AccordionDetails className={classes.details}> - {convertToList(attributes).map(({ - key, value, type, subtype, - }) => { - if (type === 'boolean') { - return ( - <Grid container direction="row" justifyContent="space-between" key={key}> - <FormControlLabel - control={( - <Checkbox - checked={value} - onChange={(e) => updateAttribute(key, e.target.checked)} - /> - )} - label={getAttributeName(key, subtype)} - /> - <IconButton size="small" className={classes.removeButton} onClick={() => deleteAttribute(key)}> - <CloseIcon fontSize="small" /> - </IconButton> - </Grid> - ); - } - return ( - <FormControl key={key}> - <InputLabel>{getAttributeName(key, subtype)}</InputLabel> - <OutlinedInput - label={getAttributeName(key, subtype)} - type={type === 'number' ? 'number' : 'text'} - value={getDisplayValue(value, subtype)} - onChange={(e) => updateAttribute(key, e.target.value, type, subtype)} - autoFocus={focusAttribute === key} - endAdornment={( - <InputAdornment position="end"> - <IconButton size="small" edge="end" onClick={() => deleteAttribute(key)}> - <CloseIcon fontSize="small" /> - </IconButton> - </InputAdornment> - )} - /> - </FormControl> - ); - })} - <Button - variant="outlined" - color="primary" - onClick={() => setAddDialogShown(true)} - startIcon={<AddIcon />} - > - {t('sharedAdd')} - </Button> - <AddAttributeDialog - open={addDialogShown} - onResult={handleAddResult} - definitions={definitions} - /> - </AccordionDetails> - </Accordion> - ); -}; - -export default EditAttributesAccordion; diff --git a/modern/src/settings/components/EditItemView.jsx b/modern/src/settings/components/EditItemView.jsx deleted file mode 100644 index 61bc4161..00000000 --- a/modern/src/settings/components/EditItemView.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Container, Button, Accordion, AccordionDetails, AccordionSummary, Skeleton, Typography, TextField, -} from '@mui/material'; -import { useCatch, useEffectAsync } from '../../reactHelper'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import PageLayout from '../../common/components/PageLayout'; -import useSettingsStyles from '../common/useSettingsStyles'; - -const EditItemView = ({ - children, endpoint, item, setItem, defaultItem, validate, onItemSaved, menu, breadcrumbs, -}) => { - const navigate = useNavigate(); - const classes = useSettingsStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - useEffectAsync(async () => { - if (!item) { - if (id) { - const response = await fetch(`/api/${endpoint}/${id}`); - if (response.ok) { - setItem(await response.json()); - } else { - throw Error(await response.text()); - } - } else { - setItem(defaultItem || {}); - } - } - }, [id, item, defaultItem]); - - const handleSave = useCatch(async () => { - let url = `/api/${endpoint}`; - if (id) { - url += `/${id}`; - } - - const response = await fetch(url, { - method: !id ? 'POST' : 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - if (onItemSaved) { - onItemSaved(await response.json()); - } - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - <PageLayout menu={menu} breadcrumbs={breadcrumbs}> - <Container maxWidth="xs" className={classes.container}> - {item ? children : ( - <Accordion defaultExpanded> - <AccordionSummary> - <Typography variant="subtitle1"> - <Skeleton width="10em" /> - </Typography> - </AccordionSummary> - <AccordionDetails> - {[...Array(3)].map((_, i) => ( - <Skeleton key={-i} width="100%"> - <TextField /> - </Skeleton> - ))} - </AccordionDetails> - </Accordion> - )} - <div className={classes.buttons}> - <Button - type="button" - color="primary" - variant="outlined" - onClick={() => navigate(-1)} - disabled={!item} - > - {t('sharedCancel')} - </Button> - <Button - type="button" - color="primary" - variant="contained" - onClick={handleSave} - disabled={!item || !validate()} - > - {t('sharedSave')} - </Button> - </div> - </Container> - </PageLayout> - ); -}; - -export default EditItemView; diff --git a/modern/src/settings/components/SearchHeader.jsx b/modern/src/settings/components/SearchHeader.jsx deleted file mode 100644 index 25757ed2..00000000 --- a/modern/src/settings/components/SearchHeader.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { TextField, useTheme, useMediaQuery } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import { useTranslation } from '../../common/components/LocalizationProvider'; - -export const filterByKeyword = (keyword) => (item) => !keyword || JSON.stringify(item).toLowerCase().includes(keyword.toLowerCase()); - -const useStyles = makeStyles((theme) => ({ - header: { - position: 'sticky', - left: 0, - display: 'flex', - flexDirection: 'column', - alignItems: 'stretch', - padding: theme.spacing(3, 2, 2), - }, -})); - -const SearchHeader = ({ keyword, setKeyword }) => { - const theme = useTheme(); - const classes = useStyles(); - const t = useTranslation(); - - const phone = useMediaQuery(theme.breakpoints.down('sm')); - - return phone ? ( - <div className={classes.header}> - <TextField - variant="outlined" - placeholder={t('sharedSearch')} - value={keyword} - onChange={(e) => setKeyword(e.target.value)} - /> - </div> - ) : ''; -}; - -export default SearchHeader; diff --git a/modern/src/settings/components/SettingsMenu.jsx b/modern/src/settings/components/SettingsMenu.jsx deleted file mode 100644 index 7085d47a..00000000 --- a/modern/src/settings/components/SettingsMenu.jsx +++ /dev/null @@ -1,173 +0,0 @@ -import React from 'react'; -import { - Divider, List, ListItemButton, ListItemIcon, ListItemText, -} from '@mui/material'; -import SettingsIcon from '@mui/icons-material/Settings'; -import CreateIcon from '@mui/icons-material/Create'; -import NotificationsIcon from '@mui/icons-material/Notifications'; -import FolderIcon from '@mui/icons-material/Folder'; -import PersonIcon from '@mui/icons-material/Person'; -import StorageIcon from '@mui/icons-material/Storage'; -import BuildIcon from '@mui/icons-material/Build'; -import PeopleIcon from '@mui/icons-material/People'; -import TodayIcon from '@mui/icons-material/Today'; -import PublishIcon from '@mui/icons-material/Publish'; -import SmartphoneIcon from '@mui/icons-material/Smartphone'; -import HelpIcon from '@mui/icons-material/Help'; -import CampaignIcon from '@mui/icons-material/Campaign'; -import { Link, useLocation } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import { - useAdministrator, useManager, useRestriction, -} from '../../common/util/permissions'; -import useFeatures from '../../common/util/useFeatures'; - -const MenuItem = ({ - title, link, icon, selected, -}) => ( - <ListItemButton key={link} component={Link} to={link} selected={selected}> - <ListItemIcon>{icon}</ListItemIcon> - <ListItemText primary={title} /> - </ListItemButton> -); - -const SettingsMenu = () => { - const t = useTranslation(); - const location = useLocation(); - - const readonly = useRestriction('readonly'); - const admin = useAdministrator(); - const manager = useManager(); - const userId = useSelector((state) => state.session.user.id); - const supportLink = useSelector((state) => state.session.server.attributes.support); - - const features = useFeatures(); - - return ( - <> - <List> - <MenuItem - title={t('sharedPreferences')} - link="/settings/preferences" - icon={<SettingsIcon />} - selected={location.pathname === '/settings/preferences'} - /> - {!readonly && ( - <> - <MenuItem - title={t('sharedNotifications')} - link="/settings/notifications" - icon={<NotificationsIcon />} - selected={location.pathname.startsWith('/settings/notification')} - /> - <MenuItem - title={t('settingsUser')} - link={`/settings/user/${userId}`} - icon={<PersonIcon />} - selected={location.pathname === `/settings/user/${userId}`} - /> - <MenuItem - title={t('deviceTitle')} - link="/settings/devices" - icon={<SmartphoneIcon />} - selected={location.pathname.startsWith('/settings/device')} - /> - <MenuItem - title={t('sharedGeofences')} - link="/geofences" - icon={<CreateIcon />} - selected={location.pathname.startsWith('/settings/geofence')} - /> - {!features.disableGroups && ( - <MenuItem - title={t('settingsGroups')} - link="/settings/groups" - icon={<FolderIcon />} - selected={location.pathname.startsWith('/settings/group')} - /> - )} - {!features.disableDrivers && ( - <MenuItem - title={t('sharedDrivers')} - link="/settings/drivers" - icon={<PersonIcon />} - selected={location.pathname.startsWith('/settings/driver')} - /> - )} - {!features.disableCalendars && ( - <MenuItem - title={t('sharedCalendars')} - link="/settings/calendars" - icon={<TodayIcon />} - selected={location.pathname.startsWith('/settings/calendar')} - /> - )} - {!features.disableComputedAttributes && ( - <MenuItem - title={t('sharedComputedAttributes')} - link="/settings/attributes" - icon={<StorageIcon />} - selected={location.pathname.startsWith('/settings/attribute')} - /> - )} - {!features.disableMaintenance && ( - <MenuItem - title={t('sharedMaintenance')} - link="/settings/maintenances" - icon={<BuildIcon />} - selected={location.pathname.startsWith('/settings/maintenance')} - /> - )} - {!features.disableSavedCommands && ( - <MenuItem - title={t('sharedSavedCommands')} - link="/settings/commands" - icon={<PublishIcon />} - selected={location.pathname.startsWith('/settings/command')} - /> - )} - {supportLink && ( - <MenuItem - title={t('settingsSupport')} - link={supportLink} - icon={<HelpIcon />} - /> - )} - </> - )} - </List> - {manager && ( - <> - <Divider /> - <List> - {admin && ( - <> - <MenuItem - title={t('serverAnnouncement')} - link="/settings/announcement" - icon={<CampaignIcon />} - selected={location.pathname === '/settings/announcement'} - /> - <MenuItem - title={t('settingsServer')} - link="/settings/server" - icon={<StorageIcon />} - selected={location.pathname === '/settings/server'} - /> - </> - )} - <MenuItem - title={t('settingsUsers')} - link="/settings/users" - icon={<PeopleIcon />} - selected={location.pathname.startsWith('/settings/user') && location.pathname !== `/settings/user/${userId}`} - /> - </List> - </> - )} - </> - ); -}; - -export default SettingsMenu; |