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,36 +1,30 @@
|
|||||||
import type { FC } from 'react';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import { Button, Typography, Box } 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 { useState, useEffect, useCallback } from 'react';
|
||||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Typography, Box } 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 { toast } from 'react-toastify';
|
||||||
|
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import SettingsEntitiesDialog from './SettingsEntitiesDialog';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import * as EMSESP from './api';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import { DeviceValueUOM_s } from './types';
|
||||||
|
import { entityItemValidation } from './validators';
|
||||||
|
import type { EntityItem } from './types';
|
||||||
|
import type { FC } from 'react';
|
||||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
||||||
|
|
||||||
import SettingsEntitiesDialog from './SettingsEntitiesDialog';
|
|
||||||
|
|
||||||
import type { EntityItem } from './types';
|
|
||||||
import { DeviceValueUOM_s } from './types';
|
|
||||||
import { extractErrorMessage } from 'utils';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { extractErrorMessage } from 'utils';
|
||||||
import * as EMSESP from './api';
|
|
||||||
import { entityItemValidation } from './validators';
|
|
||||||
|
|
||||||
const SettingsEntities: FC = () => {
|
const SettingsEntities: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [numChanges, setNumChanges] = useState<number>(0);
|
const [numChanges, setNumChanges] = useState<number>(0);
|
||||||
const blocker = useBlocker(numChanges !== 0);
|
const blocker = useBlocker(numChanges !== 0);
|
||||||
const [entities, setEntities] = useState<EntityItem[]>([]);
|
const [entities, setEntities] = useState<EntityItem[]>();
|
||||||
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
|
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const [creating, setCreating] = useState<boolean>(false);
|
const [creating, setCreating] = useState<boolean>(false);
|
||||||
@@ -52,7 +46,9 @@ const SettingsEntities: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0);
|
if (entities) {
|
||||||
|
setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0);
|
||||||
|
}
|
||||||
}, [entities]);
|
}, [entities]);
|
||||||
|
|
||||||
const entity_theme = useTheme({
|
const entity_theme = useTheme({
|
||||||
@@ -109,28 +105,24 @@ const SettingsEntities: FC = () => {
|
|||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
const setOriginalEntity = (data: EntityItem[]) => {
|
|
||||||
setEntities(
|
|
||||||
data.map((ei) => ({
|
|
||||||
...ei,
|
|
||||||
o_id: ei.id,
|
|
||||||
o_device_id: ei.device_id,
|
|
||||||
o_type_id: ei.type_id,
|
|
||||||
o_offset: ei.offset,
|
|
||||||
o_factor: ei.factor,
|
|
||||||
o_uom: ei.uom,
|
|
||||||
o_value_type: ei.value_type,
|
|
||||||
o_name: ei.name,
|
|
||||||
o_writeable: ei.writeable,
|
|
||||||
o_deleted: ei.deleted
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchEntities = useCallback(async () => {
|
const fetchEntities = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await EMSESP.readEntities();
|
const response = await EMSESP.readEntities();
|
||||||
setOriginalEntity(response.data.entities);
|
setEntities(
|
||||||
|
response.data.entities.map((ei) => ({
|
||||||
|
...ei,
|
||||||
|
o_id: ei.id,
|
||||||
|
o_device_id: ei.device_id,
|
||||||
|
o_type_id: ei.type_id,
|
||||||
|
o_offset: ei.offset,
|
||||||
|
o_factor: ei.factor,
|
||||||
|
o_uom: ei.uom,
|
||||||
|
o_value_type: ei.value_type,
|
||||||
|
o_name: ei.name,
|
||||||
|
o_writeable: ei.writeable,
|
||||||
|
o_deleted: ei.deleted
|
||||||
|
}))
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||||
}
|
}
|
||||||
@@ -160,14 +152,14 @@ const SettingsEntities: FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
toast.success(LL.SUCCESS());
|
toast.success(LL.ENTITIES_UPDATED());
|
||||||
} else {
|
} else {
|
||||||
toast.error(LL.PROBLEM_UPDATING());
|
toast.error(LL.PROBLEM_UPDATING());
|
||||||
}
|
}
|
||||||
|
void fetchEntities();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||||
}
|
}
|
||||||
setOriginalEntity(entities);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,26 +174,25 @@ const SettingsEntities: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDialogSave = (updatedItem: EntityItem) => {
|
const onDialogSave = (updatedItem: EntityItem) => {
|
||||||
if (creating) {
|
setDialogOpen(false);
|
||||||
|
if (entities && creating) {
|
||||||
setEntities([...entities.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]);
|
setEntities([...entities.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]);
|
||||||
} else {
|
} else {
|
||||||
setEntities(entities.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei)));
|
setEntities(entities?.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei)));
|
||||||
}
|
}
|
||||||
setDialogOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO need callback here too?
|
|
||||||
const addEntityItem = () => {
|
const addEntityItem = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setSelectedEntityItem({
|
setSelectedEntityItem({
|
||||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||||
|
name: '',
|
||||||
device_id: 11,
|
device_id: 11,
|
||||||
type_id: 0,
|
type_id: 0,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
factor: 1,
|
factor: 1,
|
||||||
value_type: 2,
|
|
||||||
uom: 0,
|
uom: 0,
|
||||||
name: '',
|
value_type: 2,
|
||||||
writeable: false,
|
writeable: false,
|
||||||
deleted: false
|
deleted: false
|
||||||
});
|
});
|
||||||
@@ -234,8 +225,8 @@ const SettingsEntities: FC = () => {
|
|||||||
<Header>
|
<Header>
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<HeaderCell>{LL.NAME(0)}</HeaderCell>
|
<HeaderCell>{LL.NAME(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>Device ID</HeaderCell>
|
<HeaderCell stiff>{LL.ID_OF(LL.DEVICE())}</HeaderCell>
|
||||||
<HeaderCell stiff>Type ID</HeaderCell>
|
<HeaderCell stiff>{LL.ID_OF(LL.TYPE(1))}</HeaderCell>
|
||||||
<HeaderCell stiff>Offset</HeaderCell>
|
<HeaderCell stiff>Offset</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||||
</HeaderRow>
|
</HeaderRow>
|
||||||
@@ -244,8 +235,8 @@ const SettingsEntities: FC = () => {
|
|||||||
{tableList.map((ei: EntityItem) => (
|
{tableList.map((ei: EntityItem) => (
|
||||||
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
||||||
<Cell>{ei.name}</Cell>
|
<Cell>{ei.name}</Cell>
|
||||||
<Cell>{showHex(ei.device_id, 2)}</Cell>
|
<Cell>{showHex(ei.device_id as number, 2)}</Cell>
|
||||||
<Cell>{showHex(ei.type_id, 4)}</Cell>
|
<Cell>{showHex(ei.type_id as number, 4)}</Cell>
|
||||||
<Cell>{ei.offset}</Cell>
|
<Cell>{ei.offset}</Cell>
|
||||||
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -258,11 +249,12 @@ const SettingsEntities: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.CUSTOM_ENTITIES()} titleGutter>
|
<SectionContent title={LL.CUSTOM_ENTITIES(0)} titleGutter>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
<Box mb={2} color="warning.main">
|
<Box mb={2} color="warning.main">
|
||||||
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
|
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{renderEntity()}
|
{renderEntity()}
|
||||||
|
|
||||||
{selectedEntityItem && (
|
{selectedEntityItem && (
|
||||||
@@ -272,7 +264,7 @@ const SettingsEntities: FC = () => {
|
|||||||
onClose={onDialogClose}
|
onClose={onDialogClose}
|
||||||
onSave={onDialogSave}
|
onSave={onDialogSave}
|
||||||
selectedEntityItem={selectedEntityItem}
|
selectedEntityItem={selectedEntityItem}
|
||||||
validator={entityItemValidation(entities, creating)}
|
validator={entityItemValidation()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
|
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||||
import {
|
import {
|
||||||
Grid,
|
|
||||||
Button,
|
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Checkbox,
|
Grid,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
MenuItem
|
MenuItem
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { ValidatedTextField, BlockFormControlLabel } from 'components';
|
import { DeviceValueUOM_s } from './types';
|
||||||
|
import type { EntityItem } from './types';
|
||||||
|
import type Schema from 'async-validator';
|
||||||
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
|
|
||||||
import DoneIcon from '@mui/icons-material/Done';
|
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
import { validate } from 'validators';
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
|
||||||
import type Schema from 'async-validator';
|
|
||||||
|
|
||||||
import { updateValue } from 'utils';
|
import { updateValue } from 'utils';
|
||||||
|
import { validate } from 'validators';
|
||||||
import type { EntityItem } from './types';
|
|
||||||
import { DeviceValueUOM_s } from './types';
|
|
||||||
|
|
||||||
type SettingsEntitiesDialogProps = {
|
type SettingsEntitiesDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -58,6 +55,12 @@ const SettingsEntitiesDialog = ({
|
|||||||
if (open) {
|
if (open) {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
setEditItem(selectedEntityItem);
|
setEditItem(selectedEntityItem);
|
||||||
|
// convert to hex strings straight away
|
||||||
|
setEditItem({
|
||||||
|
...selectedEntityItem,
|
||||||
|
device_id: ('0' + selectedEntityItem.device_id.toString(16).toUpperCase()).slice(-2),
|
||||||
|
type_id: ('000' + selectedEntityItem.type_id.toString(16).toUpperCase()).slice(-4)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [open, selectedEntityItem]);
|
}, [open, selectedEntityItem]);
|
||||||
|
|
||||||
@@ -69,6 +72,12 @@ const SettingsEntitiesDialog = ({
|
|||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
|
if (typeof editItem.device_id === 'string') {
|
||||||
|
editItem.device_id = parseInt(editItem.device_id, 16);
|
||||||
|
}
|
||||||
|
if (typeof editItem.type_id === 'string') {
|
||||||
|
editItem.type_id = parseInt(editItem.type_id, 16);
|
||||||
|
}
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
} catch (errors: any) {
|
} catch (errors: any) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(errors);
|
||||||
@@ -83,13 +92,12 @@ const SettingsEntitiesDialog = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={close}>
|
<Dialog open={open} onClose={close}>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()} {LL.ENTITY()}
|
{creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()} {LL.ENTITY()}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box display="flex" flexWrap="wrap" mb={1}>
|
<Box display="flex" flexWrap="wrap" mb={1}>
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap" />
|
<Box flexWrap="nowrap" whiteSpace="nowrap" />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={8}>
|
<Grid item xs={8}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
@@ -102,10 +110,9 @@ const SettingsEntitiesDialog = ({
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={4} mt={3}>
|
<Grid item xs={4} mt={3}>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={selectedEntityItem.writeable} onChange={updateFormValue} name="write" />}
|
control={<Checkbox checked={editItem.writeable} onChange={updateFormValue} name="writeable" />}
|
||||||
label={LL.WRITEABLE()}
|
label={LL.WRITEABLE()}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -113,28 +120,26 @@ const SettingsEntitiesDialog = ({
|
|||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="device_id"
|
name="device_id"
|
||||||
label="Device ID"
|
label={LL.ID_OF(LL.DEVICE())}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={editItem.device_id}
|
value={editItem.device_id}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
InputProps={{
|
inputProps={{ style: { textTransform: 'uppercase' } }}
|
||||||
startAdornment: <InputAdornment position="start">0x</InputAdornment>
|
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="type_id"
|
name="type_id"
|
||||||
label="Type ID"
|
label={LL.ID_OF(LL.TYPE(1))}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={editItem.type_id}
|
value={editItem.type_id}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
InputProps={{
|
inputProps={{ style: { textTransform: 'uppercase' } }}
|
||||||
startAdornment: <InputAdornment position="start">0x</InputAdornment>
|
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
@@ -169,7 +174,8 @@ const SettingsEntitiesDialog = ({
|
|||||||
<MenuItem value={6}>TIME</MenuItem>
|
<MenuItem value={6}>TIME</MenuItem>
|
||||||
</ValidatedTextField>
|
</ValidatedTextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
{selectedEntityItem.value_type !== 0 && (
|
|
||||||
|
{editItem.value_type !== 0 && (
|
||||||
<>
|
<>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
|
|||||||
@@ -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 { useState, useEffect, useCallback } from 'react';
|
||||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
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 { toast } from 'react-toastify';
|
||||||
|
import SettingsSchedulerDialog from './SettingsSchedulerDialog';
|
||||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
import * as EMSESP from './api';
|
||||||
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 { ScheduleFlag } from './types';
|
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 { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { extractErrorMessage } from 'utils';
|
||||||
import * as EMSESP from './api';
|
|
||||||
|
|
||||||
function makeid() {
|
|
||||||
return Math.floor(Math.random() * (Math.floor(200) - 100) + 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsScheduler: FC = () => {
|
const SettingsScheduler: FC = () => {
|
||||||
const { LL, locale } = useI18nContext();
|
const { LL, locale } = useI18nContext();
|
||||||
|
|
||||||
const [numChanges, setNumChanges] = useState<number>(0);
|
const [numChanges, setNumChanges] = useState<number>(0);
|
||||||
const blocker = useBlocker(numChanges !== 0);
|
const blocker = useBlocker(numChanges !== 0);
|
||||||
|
const [schedule, setSchedule] = useState<ScheduleItem[]>([]);
|
||||||
const emptySchedule = {
|
const [selectedScheduleItem, setSelectedScheduleItem] = useState<ScheduleItem>();
|
||||||
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 [dow, setDow] = useState<string[]>([]);
|
const [dow, setDow] = useState<string[]>([]);
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const [creating, setCreating] = useState<boolean>(false);
|
const [creating, setCreating] = useState<boolean>(false);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasScheduleChanged(si: ScheduleItem) {
|
function hasScheduleChanged(si: ScheduleItem) {
|
||||||
return (
|
return (
|
||||||
@@ -107,17 +45,11 @@ const SettingsScheduler: FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO fix
|
|
||||||
const getNumChanges = () => {
|
|
||||||
if (!schedule) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return schedule.filter((si) => hasScheduleChanged(si)).length;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNumChanges(getNumChanges());
|
if (schedule) {
|
||||||
});
|
setNumChanges(schedule ? schedule.filter((si) => hasScheduleChanged(si)).length : 0);
|
||||||
|
}
|
||||||
|
}, [schedule]);
|
||||||
|
|
||||||
const schedule_theme = useTheme({
|
const schedule_theme = useTheme({
|
||||||
Table: `
|
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 () => {
|
const fetchSchedule = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await EMSESP.readSchedule();
|
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) {
|
} catch (error) {
|
||||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||||
}
|
}
|
||||||
}, [LL]);
|
}, [LL]);
|
||||||
|
|
||||||
|
// on mount
|
||||||
useEffect(() => {
|
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();
|
void fetchSchedule();
|
||||||
setDow(getDayNames());
|
}, [locale, fetchSchedule]);
|
||||||
}, [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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSchedule = async () => {
|
const saveSchedule = async () => {
|
||||||
if (schedule) {
|
if (schedule) {
|
||||||
@@ -248,74 +145,40 @@ const SettingsScheduler: FC = () => {
|
|||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
toast.success(LL.SCHEDULE_SAVED());
|
toast.success(LL.SCHEDULE_UPDATED());
|
||||||
} else {
|
} else {
|
||||||
toast.error(LL.PROBLEM_UPDATING());
|
toast.error(LL.PROBLEM_UPDATING());
|
||||||
}
|
}
|
||||||
|
void fetchSchedule();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||||
}
|
}
|
||||||
setOriginalSchedule(schedule);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFlagName(flag: number) {
|
const editScheduleItem = useCallback((si: ScheduleItem) => {
|
||||||
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 = '';
|
|
||||||
}
|
|
||||||
setCreating(false);
|
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 = () => {
|
const addScheduleItem = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setScheduleItem({
|
setSelectedScheduleItem({
|
||||||
id: makeid(),
|
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||||
active: false,
|
active: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
flags: 0,
|
flags: 0,
|
||||||
@@ -324,13 +187,7 @@ const SettingsScheduler: FC = () => {
|
|||||||
value: '',
|
value: '',
|
||||||
name: ''
|
name: ''
|
||||||
});
|
});
|
||||||
};
|
setDialogOpen(true);
|
||||||
|
|
||||||
const updateScheduleItem = () => {
|
|
||||||
if (scheduleItem) {
|
|
||||||
setSchedule([...schedule.filter((si) => creating || si.o_id !== scheduleItem.o_id), scheduleItem]);
|
|
||||||
}
|
|
||||||
setScheduleItem(undefined);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSchedule = () => {
|
const renderSchedule = () => {
|
||||||
@@ -338,6 +195,17 @@ const SettingsScheduler: FC = () => {
|
|||||||
return <FormLoader errorMessage={errorMessage} />;
|
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 (
|
return (
|
||||||
<Table
|
<Table
|
||||||
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
|
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 (
|
return (
|
||||||
<SectionContent title={LL.SCHEDULER()} titleGutter>
|
<SectionContent title={LL.SCHEDULER()} titleGutter>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
@@ -562,7 +263,19 @@ const SettingsScheduler: FC = () => {
|
|||||||
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
|
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{renderSchedule()}
|
{renderSchedule()}
|
||||||
{renderEditSchedule()}
|
|
||||||
|
{selectedScheduleItem && (
|
||||||
|
<SettingsSchedulerDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
creating={creating}
|
||||||
|
onClose={onDialogClose}
|
||||||
|
onSave={onDialogSave}
|
||||||
|
selectedSchedulerItem={selectedScheduleItem}
|
||||||
|
validator={schedulerItemValidation()}
|
||||||
|
dow={dow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
{numChanges !== 0 && (
|
{numChanges !== 0 && (
|
||||||
|
|||||||
254
interface/src/project/SettingsSchedulerDialog.tsx
Normal file
254
interface/src/project/SettingsSchedulerDialog.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
|
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
TextField,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { ScheduleFlag } from './types';
|
||||||
|
import type { ScheduleItem } from './types';
|
||||||
|
import type Schema from 'async-validator';
|
||||||
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
|
|
||||||
|
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||||
|
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
|
import { updateValue } from 'utils';
|
||||||
|
import { validate } from 'validators';
|
||||||
|
|
||||||
|
type SettingsSchedulerDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
creating: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (ei: ScheduleItem) => void;
|
||||||
|
selectedSchedulerItem: ScheduleItem;
|
||||||
|
validator: Schema;
|
||||||
|
dow: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsSchedulerDialog = ({
|
||||||
|
open,
|
||||||
|
creating,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
selectedSchedulerItem,
|
||||||
|
validator,
|
||||||
|
dow
|
||||||
|
}: SettingsSchedulerDialogProps) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
const [editItem, setEditItem] = useState<ScheduleItem>(selectedSchedulerItem);
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
setEditItem(selectedSchedulerItem);
|
||||||
|
}
|
||||||
|
}, [open, selectedSchedulerItem]);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
await validate(validator, editItem);
|
||||||
|
onSave(editItem);
|
||||||
|
} catch (errors: any) {
|
||||||
|
setFieldErrors(errors);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
editItem.deleted = true;
|
||||||
|
onSave(editItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFlag = (si: ScheduleItem, flag: number) => (
|
||||||
|
<Typography variant="button" sx={{ fontSize: 10 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
|
||||||
|
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
|
||||||
|
const isTimer = editItem.flags === ScheduleFlag.SCHEDULE_TIMER;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={close}>
|
||||||
|
<DialogTitle>
|
||||||
|
{creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()} {LL.ENTITY()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box display="flex" flexWrap="wrap" mb={1}>
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
value={getFlagString(editItem.flags)}
|
||||||
|
onChange={(event, flag) => {
|
||||||
|
setEditItem({ ...editItem, flags: getFlagNumber(flag) & 127 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="2">{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}</ToggleButton>
|
||||||
|
<ToggleButton value="4">{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}</ToggleButton>
|
||||||
|
<ToggleButton value="8">{showFlag(editItem, ScheduleFlag.SCHEDULE_WED)}</ToggleButton>
|
||||||
|
<ToggleButton value="16">{showFlag(editItem, ScheduleFlag.SCHEDULE_THU)}</ToggleButton>
|
||||||
|
<ToggleButton value="32">{showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)}</ToggleButton>
|
||||||
|
<ToggleButton value="64">{showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)}</ToggleButton>
|
||||||
|
<ToggleButton value="1">{showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)}</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
|
{isTimer ? (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
sx={{ bgcolor: '#334f65' }}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setEditItem({ ...editItem, flags: 0 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
setEditItem({ ...editItem, flags: ScheduleFlag.SCHEDULE_TIMER });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Grid container>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={<Checkbox checked={editItem.active} onChange={updateFormValue} name="active" />}
|
||||||
|
label={LL.ACTIVE()}
|
||||||
|
/>
|
||||||
|
{editItem.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={editItem.time}
|
||||||
|
margin="normal"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
{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={editItem.cmd}
|
||||||
|
margin="normal"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
name="value"
|
||||||
|
label={LL.VALUE(0)}
|
||||||
|
multiline
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
value={editItem.value}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="name"
|
||||||
|
label={LL.NAME(0)}
|
||||||
|
value={editItem.name}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{!creating && (
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
|
||||||
|
{LL.REMOVE()}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary">
|
||||||
|
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsSchedulerDialog;
|
||||||
@@ -60,7 +60,7 @@ export interface Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
id: string; // id index
|
id: number; // id index
|
||||||
tn: string; // device type translated name
|
tn: string; // device type translated name
|
||||||
t: number; // device type id
|
t: number; // device type id
|
||||||
b: string; // brand
|
b: string; // brand
|
||||||
@@ -341,19 +341,19 @@ export enum ScheduleFlag {
|
|||||||
export interface EntityItem {
|
export interface EntityItem {
|
||||||
id: number; // unique number
|
id: number; // unique number
|
||||||
name: string;
|
name: string;
|
||||||
device_id: number;
|
device_id: number | string;
|
||||||
type_id: number;
|
type_id: number | string;
|
||||||
offset: number;
|
offset: number;
|
||||||
factor: number;
|
factor: number;
|
||||||
uom: number;
|
uom: number;
|
||||||
value_type: number;
|
value_type: number;
|
||||||
value?: number;
|
value?: number; // optional
|
||||||
writeable: boolean;
|
writeable: boolean;
|
||||||
deleted?: boolean; // optional
|
deleted?: boolean; // optional
|
||||||
o_id?: number;
|
o_id?: number;
|
||||||
o_name?: string;
|
o_name?: string;
|
||||||
o_device_id?: number;
|
o_device_id?: number | string;
|
||||||
o_type_id?: number;
|
o_type_id?: number | string;
|
||||||
o_offset?: number;
|
o_offset?: number;
|
||||||
o_factor?: number;
|
o_factor?: number;
|
||||||
o_uom?: number;
|
o_uom?: number;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { InternalRuleItem } from 'async-validator';
|
|
||||||
import Schema from 'async-validator';
|
import Schema from 'async-validator';
|
||||||
|
import type { Settings } from './types';
|
||||||
|
import type { InternalRuleItem } from 'async-validator';
|
||||||
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
|
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
|
||||||
import type { Settings, ScheduleItem, EntityItem } from './types';
|
|
||||||
|
|
||||||
export const GPIO_VALIDATOR = {
|
export const GPIO_VALIDATOR = {
|
||||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
||||||
@@ -86,25 +86,14 @@ export const createSettingsValidator = (settings: Settings) =>
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
|
export const schedulerItemValidation = () =>
|
||||||
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
|
|
||||||
if (name && o_name && o_name !== name && schedule.find((si) => si.name === name)) {
|
|
||||||
callback('Name already in use');
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) =>
|
|
||||||
new Schema({
|
new Schema({
|
||||||
name: [
|
name: [
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
pattern: /^[a-zA-Z0-9_\\.]{0,15}$/,
|
pattern: /^[a-zA-Z0-9_\\.]{0,15}$/,
|
||||||
message: "Must be <15 characters: alpha numeric, '_' or '.'"
|
message: "Must be <15 characters: alpha numeric, '_' or '.'"
|
||||||
},
|
}
|
||||||
...[uniqueNameValidator(schedule, scheduleItem.o_name)]
|
|
||||||
],
|
],
|
||||||
cmd: [
|
cmd: [
|
||||||
{ required: true, message: 'Command is required' },
|
{ required: true, message: 'Command is required' },
|
||||||
@@ -112,7 +101,7 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
export const entityItemValidation = (entities: EntityItem[], creating: boolean) =>
|
export const entityItemValidation = () =>
|
||||||
new Schema({
|
new Schema({
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: 'Name is required' },
|
{ required: true, message: 'Name is required' },
|
||||||
@@ -122,13 +111,30 @@ export const entityItemValidation = (entities: EntityItem[], creating: boolean)
|
|||||||
message: "Must be <15 characters: alpha numeric, '_' or '.'"
|
message: "Must be <15 characters: alpha numeric, '_' or '.'"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
device_id: [{ type: 'hex', required: true, message: 'ID must be a hex value' }]
|
device_id: [
|
||||||
// type_id: [
|
{ required: true, message: 'Device ID is required' },
|
||||||
// { required: true, message: 'Type_id is required' },
|
{
|
||||||
// { type: 'string', pattern: /^[A-F0-9]{1,4}$/, message: 'Must be a hex number' }
|
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
|
||||||
// ],
|
if (isNaN(parseInt(value, 16))) {
|
||||||
// offset: [
|
callback('Must be a hex number');
|
||||||
// { required: true, message: 'Offset is required' },
|
}
|
||||||
// { type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' }
|
callback();
|
||||||
// ]
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
type_id: [
|
||||||
|
{ required: true, message: 'Type ID is required' },
|
||||||
|
{
|
||||||
|
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
|
||||||
|
if (isNaN(parseInt(value, 16))) {
|
||||||
|
callback('Must be a hex number');
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
offset: [
|
||||||
|
{ required: true, message: 'Offset is required' },
|
||||||
|
{ type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' }
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user