mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
refactor diallogs
This commit is contained in:
@@ -1,98 +1,36 @@
|
||||
import type { FC } from 'react';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
|
||||
import { Box, Typography, Divider, Stack, Button } from '@mui/material';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Stack,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Checkbox,
|
||||
Grid,
|
||||
TextField,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
|
||||
import {
|
||||
ValidatedTextField,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
BlockFormControlLabel,
|
||||
SectionContent,
|
||||
BlockNavigation
|
||||
} from 'components';
|
||||
|
||||
import { extractErrorMessage, updateValue } from 'utils';
|
||||
|
||||
import { validate } from 'validators';
|
||||
import { schedulerItemValidation } from './validators';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import type { ScheduleItem } from './types';
|
||||
import SettingsSchedulerDialog from './SettingsSchedulerDialog';
|
||||
import * as EMSESP from './api';
|
||||
import { ScheduleFlag } from './types';
|
||||
import { schedulerItemValidation } from './validators';
|
||||
import type { ScheduleItem } from './types';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
function makeid() {
|
||||
return Math.floor(Math.random() * (Math.floor(200) - 100) + 100);
|
||||
}
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
const SettingsScheduler: FC = () => {
|
||||
const { LL, locale } = useI18nContext();
|
||||
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
|
||||
const emptySchedule = {
|
||||
id: 0,
|
||||
active: false,
|
||||
deleted: false,
|
||||
flags: 0,
|
||||
time: '12:00',
|
||||
cmd: '',
|
||||
value: '',
|
||||
name: '',
|
||||
o_name: ''
|
||||
};
|
||||
|
||||
const [schedule, setSchedule] = useState<ScheduleItem[]>([emptySchedule]);
|
||||
const [scheduleItem, setScheduleItem] = useState<ScheduleItem>();
|
||||
const [schedule, setSchedule] = useState<ScheduleItem[]>([]);
|
||||
const [selectedScheduleItem, setSelectedScheduleItem] = useState<ScheduleItem>();
|
||||
const [dow, setDow] = useState<string[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
// eslint-disable-next-line
|
||||
const [flags, setFlags] = useState(() => ['']);
|
||||
|
||||
function getDayNames() {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
|
||||
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
||||
const dd = day < 10 ? `0${day}` : day;
|
||||
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
||||
});
|
||||
return days.map((date) => formatter.format(date));
|
||||
}
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
function hasScheduleChanged(si: ScheduleItem) {
|
||||
return (
|
||||
@@ -107,17 +45,11 @@ const SettingsScheduler: FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO fix
|
||||
const getNumChanges = () => {
|
||||
if (!schedule) {
|
||||
return 0;
|
||||
}
|
||||
return schedule.filter((si) => hasScheduleChanged(si)).length;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNumChanges(getNumChanges());
|
||||
});
|
||||
if (schedule) {
|
||||
setNumChanges(schedule ? schedule.filter((si) => hasScheduleChanged(si)).length : 0);
|
||||
}
|
||||
}, [schedule]);
|
||||
|
||||
const schedule_theme = useTheme({
|
||||
Table: `
|
||||
@@ -164,72 +96,37 @@ const SettingsScheduler: FC = () => {
|
||||
`
|
||||
});
|
||||
|
||||
const setOriginalSchedule = (data: ScheduleItem[]) => {
|
||||
setSchedule(
|
||||
data.map((si) => ({
|
||||
...si,
|
||||
o_id: si.id,
|
||||
o_active: si.active,
|
||||
o_deleted: si.deleted,
|
||||
o_flags: si.flags,
|
||||
o_time: si.time,
|
||||
o_cmd: si.cmd,
|
||||
o_value: si.value,
|
||||
o_name: si.name
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const fetchSchedule = useCallback(async () => {
|
||||
try {
|
||||
const response = await EMSESP.readSchedule();
|
||||
setOriginalSchedule(response.data.schedule);
|
||||
setSchedule(
|
||||
response.data.schedule.map((si) => ({
|
||||
...si,
|
||||
o_id: si.id,
|
||||
o_active: si.active,
|
||||
o_deleted: si.deleted,
|
||||
o_flags: si.flags,
|
||||
o_time: si.time,
|
||||
o_cmd: si.cmd,
|
||||
o_value: si.value,
|
||||
o_name: si.name
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, [LL]);
|
||||
|
||||
// on mount
|
||||
useEffect(() => {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
|
||||
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
||||
const dd = day < 10 ? `0${day}` : day;
|
||||
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
||||
});
|
||||
setDow(days.map((date) => formatter.format(date)));
|
||||
void fetchSchedule();
|
||||
setDow(getDayNames());
|
||||
}, [getDayNames, fetchSchedule]);
|
||||
|
||||
const getFlagNumber = (newFlag: string[]) => {
|
||||
let new_flag = 0;
|
||||
for (const entry of newFlag) {
|
||||
new_flag |= Number(entry);
|
||||
}
|
||||
return new_flag;
|
||||
};
|
||||
|
||||
const getFlagString = (f: number) => {
|
||||
const new_flags: string[] = [];
|
||||
if ((f & 1) === 1) {
|
||||
new_flags.push('1');
|
||||
}
|
||||
if ((f & 2) === 2) {
|
||||
new_flags.push('2');
|
||||
}
|
||||
if ((f & 4) === 4) {
|
||||
new_flags.push('4');
|
||||
}
|
||||
if ((f & 8) === 8) {
|
||||
new_flags.push('8');
|
||||
}
|
||||
if ((f & 16) === 16) {
|
||||
new_flags.push('16');
|
||||
}
|
||||
if ((f & 32) === 32) {
|
||||
new_flags.push('32');
|
||||
}
|
||||
if ((f & 64) === 64) {
|
||||
new_flags.push('64');
|
||||
}
|
||||
if ((f & 128) === 128) {
|
||||
new_flags.push('128');
|
||||
}
|
||||
return new_flags;
|
||||
};
|
||||
}, [locale, fetchSchedule]);
|
||||
|
||||
const saveSchedule = async () => {
|
||||
if (schedule) {
|
||||
@@ -248,74 +145,40 @@ const SettingsScheduler: FC = () => {
|
||||
}))
|
||||
});
|
||||
if (response.status === 200) {
|
||||
toast.success(LL.SCHEDULE_SAVED());
|
||||
toast.success(LL.SCHEDULE_UPDATED());
|
||||
} else {
|
||||
toast.error(LL.PROBLEM_UPDATING());
|
||||
}
|
||||
void fetchSchedule();
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
}
|
||||
setOriginalSchedule(schedule);
|
||||
}
|
||||
};
|
||||
|
||||
function getFlagName(flag: number) {
|
||||
if ((flag & ScheduleFlag.SCHEDULE_MON) === ScheduleFlag.SCHEDULE_MON) {
|
||||
return dow[1];
|
||||
}
|
||||
if ((flag & ScheduleFlag.SCHEDULE_TUE) === ScheduleFlag.SCHEDULE_TUE) {
|
||||
return dow[2];
|
||||
}
|
||||
if ((flag & ScheduleFlag.SCHEDULE_WED) === ScheduleFlag.SCHEDULE_WED) {
|
||||
return dow[3];
|
||||
}
|
||||
if ((flag & ScheduleFlag.SCHEDULE_THU) === ScheduleFlag.SCHEDULE_THU) {
|
||||
return dow[4];
|
||||
}
|
||||
if ((flag & ScheduleFlag.SCHEDULE_FRI) === ScheduleFlag.SCHEDULE_FRI) {
|
||||
return dow[5];
|
||||
}
|
||||
if ((flag & ScheduleFlag.SCHEDULE_SAT) === ScheduleFlag.SCHEDULE_SAT) {
|
||||
return dow[6];
|
||||
}
|
||||
if ((flag & ScheduleFlag.SCHEDULE_SUN) === ScheduleFlag.SCHEDULE_SUN) {
|
||||
return dow[0];
|
||||
}
|
||||
if ((flag & ScheduleFlag.SCHEDULE_TIMER) === ScheduleFlag.SCHEDULE_TIMER) {
|
||||
return LL.TIMER(0);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const dayBox = (si: ScheduleItem, flag: number) => (
|
||||
<>
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
|
||||
{getFlagName(flag)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
</>
|
||||
);
|
||||
|
||||
const showFlag = (si: ScheduleItem, flag: number) => (
|
||||
<Typography variant="button" sx={{ fontSize: 10 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
|
||||
{getFlagName(flag)}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const editScheduleItem = (si: ScheduleItem) => {
|
||||
if (si.name === undefined) {
|
||||
si.name = '';
|
||||
}
|
||||
const editScheduleItem = useCallback((si: ScheduleItem) => {
|
||||
setCreating(false);
|
||||
setScheduleItem(si);
|
||||
setSelectedScheduleItem(si);
|
||||
setDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const onDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const onDialogSave = (updatedItem: ScheduleItem) => {
|
||||
setDialogOpen(false);
|
||||
if (schedule && creating) {
|
||||
setSchedule([...schedule.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]);
|
||||
} else {
|
||||
setSchedule(schedule?.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si)));
|
||||
}
|
||||
};
|
||||
|
||||
const addScheduleItem = () => {
|
||||
setCreating(true);
|
||||
setScheduleItem({
|
||||
id: makeid(),
|
||||
setSelectedScheduleItem({
|
||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||
active: false,
|
||||
deleted: false,
|
||||
flags: 0,
|
||||
@@ -324,13 +187,7 @@ const SettingsScheduler: FC = () => {
|
||||
value: '',
|
||||
name: ''
|
||||
});
|
||||
};
|
||||
|
||||
const updateScheduleItem = () => {
|
||||
if (scheduleItem) {
|
||||
setSchedule([...schedule.filter((si) => creating || si.o_id !== scheduleItem.o_id), scheduleItem]);
|
||||
}
|
||||
setScheduleItem(undefined);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const renderSchedule = () => {
|
||||
@@ -338,6 +195,17 @@ const SettingsScheduler: FC = () => {
|
||||
return <FormLoader errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const dayBox = (si: ScheduleItem, flag: number) => (
|
||||
<>
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
|
||||
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
|
||||
@@ -388,173 +256,6 @@ const SettingsScheduler: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const removeScheduleItem = (si: ScheduleItem) => {
|
||||
si.deleted = true;
|
||||
setScheduleItem(si);
|
||||
updateScheduleItem();
|
||||
};
|
||||
|
||||
const validateScheduleItem = async () => {
|
||||
if (scheduleItem) {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(schedulerItemValidation(schedule, scheduleItem), scheduleItem);
|
||||
updateScheduleItem();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setScheduleItem(undefined);
|
||||
setFieldErrors(undefined);
|
||||
};
|
||||
|
||||
const renderEditSchedule = () => {
|
||||
if (scheduleItem) {
|
||||
const isTimer = scheduleItem.flags === ScheduleFlag.SCHEDULE_TIMER;
|
||||
return (
|
||||
<Dialog open={!!scheduleItem} onClose={() => closeDialog()}>
|
||||
<DialogTitle>
|
||||
{creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()} {LL.SCHEDULE(1)}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box display="flex" flexWrap="wrap" mb={1}>
|
||||
<Box flexGrow={1}>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
color="secondary"
|
||||
value={getFlagString(scheduleItem.flags)}
|
||||
onChange={(event, flag) => {
|
||||
scheduleItem.flags = getFlagNumber(flag) & 127;
|
||||
setFlags(['']); // forces refresh
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="2">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_MON)}</ToggleButton>
|
||||
<ToggleButton value="4">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_TUE)}</ToggleButton>
|
||||
<ToggleButton value="8">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_WED)}</ToggleButton>
|
||||
<ToggleButton value="16">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_THU)}</ToggleButton>
|
||||
<ToggleButton value="32">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_FRI)}</ToggleButton>
|
||||
<ToggleButton value="64">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_SAT)}</ToggleButton>
|
||||
<ToggleButton value="1">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_SUN)}</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
{isTimer ? (
|
||||
<Button
|
||||
size="large"
|
||||
sx={{ bgcolor: '#334f65' }}
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
scheduleItem.flags = 0;
|
||||
setFlags(['']); // forces refresh
|
||||
}}
|
||||
>
|
||||
{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_TIMER)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="large"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
scheduleItem.flags = ScheduleFlag.SCHEDULE_TIMER;
|
||||
setFlags(['']); // forces refresh
|
||||
}}
|
||||
>
|
||||
{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_TIMER)}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Grid container>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox checked={scheduleItem.active} onChange={updateValue(setScheduleItem)} name="active" />
|
||||
}
|
||||
label={LL.ACTIVE()}
|
||||
/>
|
||||
{scheduleItem.active && (
|
||||
<Grid item sx={{ mt: 1 }}>
|
||||
<CheckCircleIcon sx={{ color: '#79D200', fontSize: 16, verticalAlign: 'middle' }} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid container>
|
||||
<TextField
|
||||
name="time"
|
||||
type="time"
|
||||
label={isTimer ? LL.TIMER(1) : LL.TIME(1)}
|
||||
value={scheduleItem.time}
|
||||
margin="normal"
|
||||
onChange={updateValue(setScheduleItem)}
|
||||
/>
|
||||
{isTimer && (
|
||||
<Box color="warning.main" ml={2} mt={4}>
|
||||
<Typography variant="body2">{LL.SCHEDULER_HELP_2()}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="cmd"
|
||||
label={LL.COMMAND(0)}
|
||||
fullWidth
|
||||
value={scheduleItem.cmd}
|
||||
margin="normal"
|
||||
onChange={updateValue(setScheduleItem)}
|
||||
/>
|
||||
<TextField
|
||||
name="value"
|
||||
label={LL.VALUE(0)}
|
||||
multiline
|
||||
margin="normal"
|
||||
fullWidth
|
||||
value={scheduleItem.value}
|
||||
onChange={updateValue(setScheduleItem)}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="name"
|
||||
label={LL.NAME(0)}
|
||||
value={scheduleItem.name}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
onChange={updateValue(setScheduleItem)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!creating && (
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
||||
<Button
|
||||
startIcon={<RemoveIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => removeScheduleItem(scheduleItem)}
|
||||
>
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => closeDialog()} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={creating ? <AddIcon /> : <DoneIcon />}
|
||||
variant="outlined"
|
||||
type="submit"
|
||||
onClick={() => validateScheduleItem()}
|
||||
color="primary"
|
||||
>
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SCHEDULER()} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
@@ -562,7 +263,19 @@ const SettingsScheduler: FC = () => {
|
||||
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
|
||||
</Box>
|
||||
{renderSchedule()}
|
||||
{renderEditSchedule()}
|
||||
|
||||
{selectedScheduleItem && (
|
||||
<SettingsSchedulerDialog
|
||||
open={dialogOpen}
|
||||
creating={creating}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
selectedSchedulerItem={selectedScheduleItem}
|
||||
validator={schedulerItemValidation()}
|
||||
dow={dow}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges !== 0 && (
|
||||
|
||||
Reference in New Issue
Block a user