aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-08-06 07:24:13 -0700
committerAnton Tananaev <anton@traccar.org>2022-08-06 07:24:13 -0700
commit6975055fd388c813e730a9be65f6fc96ad5f213e (patch)
treef81d93689b32235edd9ea5493aeb90bbedcd1e3c
parent7347b398aae694c58feb1a48944778a7c581dc72 (diff)
downloadtrackermap-web-6975055fd388c813e730a9be65f6fc96ad5f213e.tar.gz
trackermap-web-6975055fd388c813e730a9be65f6fc96ad5f213e.tar.bz2
trackermap-web-6975055fd388c813e730a9be65f6fc96ad5f213e.zip
Support direct calendar creation
-rw-r--r--modern/src/settings/CalendarPage.js132
-rw-r--r--web/l10n/en.json14
2 files changed, 137 insertions, 9 deletions
diff --git a/modern/src/settings/CalendarPage.js b/modern/src/settings/CalendarPage.js
index 27fdb462..862aa9a3 100644
--- a/modern/src/settings/CalendarPage.js
+++ b/modern/src/settings/CalendarPage.js
@@ -1,7 +1,8 @@
+import moment from 'moment';
import React, { useState } from 'react';
import TextField from '@mui/material/TextField';
import {
- Accordion, AccordionSummary, AccordionDetails, Typography,
+ Accordion, AccordionSummary, AccordionDetails, Typography, FormControl, InputLabel, Select, MenuItem,
} from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
@@ -10,6 +11,49 @@ 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';
+
+const formatTime = (time) => {
+ const tzid = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ return `TZID=${tzid}:${time.format('YYYYMMDDTHHmmss')}`;
+};
+
+const parseRule = (rule) => {
+ const fragments = rule.split(';');
+ const frequency = fragments[0].includes('FREQ') ? fragments[0].substring(11) : 'ONCE';
+ 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:';
+ }
+};
+
+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;${formatTime(moment())}`,
+ `DTEND;${formatTime(moment().add(1, 'hours'))}`,
+ 'RRULE:FREQ=DAILY',
+ 'SUMMARY:Event',
+ 'END:VEVENT',
+ 'END:VCALENDAR',
+].join('\n'));
const useStyles = makeStyles((theme) => ({
details: {
@@ -26,6 +70,14 @@ const CalendarPage = () => {
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();
@@ -34,8 +86,6 @@ const CalendarPage = () => {
setItem({ ...item, data: result.substr(result.indexOf(',') + 1) });
};
reader.readAsDataURL(files[0]);
- } else {
- setItem({ ...item, data: null });
}
};
@@ -46,6 +96,7 @@ const CalendarPage = () => {
endpoint="calendars"
item={item}
setItem={setItem}
+ defaultItem={{ data: simpleCalendar() }}
validate={validate}
menu={<SettingsMenu />}
breadcrumbs={['settingsTitle', 'sharedCalendar']}
@@ -64,12 +115,75 @@ const CalendarPage = () => {
onChange={(event) => setItem({ ...item, name: event.target.value })}
label={t('sharedName')}
/>
- <DropzoneArea
- dropzoneText={t('sharedDropzoneText')}
- filesLimit={1}
- onChange={handleFiles}
- showAlerts={false}
- />
+ <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={moment(lines[5].slice(-15)).locale('en').format(moment.HTML5_FMT.DATETIME_LOCAL)}
+ onChange={(e) => {
+ const time = formatTime(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL));
+ setItem({ ...item, data: updateCalendar(lines, 5, `DTSTART;${time}`) });
+ }}
+ />
+ <TextField
+ label={t('reportTo')}
+ type="datetime-local"
+ value={moment(lines[6].slice(-15)).locale('en').format(moment.HTML5_FMT.DATETIME_LOCAL)}
+ onChange={(e) => {
+ const time = formatTime(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL));
+ 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
diff --git a/web/l10n/en.json b/web/l10n/en.json
index c23cb402..2f2ee27b 100644
--- a/web/l10n/en.json
+++ b/web/l10n/en.json
@@ -96,6 +96,20 @@
"sharedImport": "Import",
"sharedColumns": "Columns",
"sharedDropzoneText": "Drag and drop a file here or click",
+ "calendarSimple": "Simple",
+ "calendarRecurrence": "Recurrence",
+ "calendarOnce": "Once",
+ "calendarDaily": "Daily",
+ "calendarWeekly": "Weekly",
+ "calendarMonthly": "Monthly",
+ "calendarDays": "Days",
+ "calendarSunday": "Sunday",
+ "calendarMonday": "Monday",
+ "calendarTuesday": "Tuesday",
+ "calendarWednesday": "Wednesday",
+ "calendarThursday": "Thursday",
+ "calendarFriday": "Friday",
+ "calendarSaturday": "Saturday",
"attributeSpeedLimit": "Speed Limit",
"attributeFuelDropThreshold": "Fuel Drop Threshold",
"attributeFuelIncreaseThreshold": "Fuel Increase Threshold",