import { useEffect, useMemo, useState } from 'react'; import AddIcon from '@mui/icons-material/Add'; import CancelIcon from '@mui/icons-material/Cancel'; import DoneIcon from '@mui/icons-material/Done'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined'; import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, Grid, TextField, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; import { dialogStyle } from 'CustomTheme'; 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 { ValidationError, validate } from 'validators'; import { ScheduleFlag } from './types'; import type { ScheduleItem } from './types'; // Constants const FLAG_MASK_127 = 127; const SCHEDULE_TYPE_THRESHOLD = 127; const FLAG_ALL_DAYS = 127; const DEFAULT_TIME = '00:00'; const TYPOGRAPHY_FONT_SIZE = 10; // Day of week flag configuration (static, defined outside component) const DAY_FLAGS = [ { value: '2', flag: ScheduleFlag.SCHEDULE_MON }, { value: '4', flag: ScheduleFlag.SCHEDULE_TUE }, { value: '8', flag: ScheduleFlag.SCHEDULE_WED }, { value: '16', flag: ScheduleFlag.SCHEDULE_THU }, { value: '32', flag: ScheduleFlag.SCHEDULE_FRI }, { value: '64', flag: ScheduleFlag.SCHEDULE_SAT }, { value: '1', flag: ScheduleFlag.SCHEDULE_SUN } ] as const; // Day of week flag values array (static) const FLAG_VALUES = [ ScheduleFlag.SCHEDULE_SUN, ScheduleFlag.SCHEDULE_MON, ScheduleFlag.SCHEDULE_TUE, ScheduleFlag.SCHEDULE_WED, ScheduleFlag.SCHEDULE_THU, ScheduleFlag.SCHEDULE_FRI, ScheduleFlag.SCHEDULE_SAT ] as const; const getFlagDOWnumber = (flags: string[]) => flags.reduce((acc, flag) => acc | Number(flag), 0) & FLAG_MASK_127; const getFlagDOWstring = (f: number) => FLAG_VALUES.filter((flag) => (f & flag) === flag).map((flag) => String(flag)); interface SchedulerDialogProps { open: boolean; creating: boolean; onClose: () => void; onSave: (ei: ScheduleItem) => void; selectedItem: ScheduleItem; validator: Schema; dow: string[]; } const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, validator, dow }: SchedulerDialogProps) => { const { LL } = useI18nContext(); const [editItem, setEditItem] = useState(selectedItem); const [fieldErrors, setFieldErrors] = useState(); const [scheduleType, setScheduleType] = useState(); // Stable handler reference so the memoized ValidatedTextField can skip re-renders const updateFormValue = useMemo( () => updateValue( setEditItem as unknown as React.Dispatch< React.SetStateAction> > ), [] ); useEffect(() => { if (open) { setFieldErrors(undefined); setEditItem(selectedItem); // Set the flags based on type when page is loaded: // 0-127 is day schedule // 128 is timer // 129 is on change // 130 is on condition // 132 is immediate setScheduleType( selectedItem.flags <= SCHEDULE_TYPE_THRESHOLD ? ScheduleFlag.SCHEDULE_DAY : selectedItem.flags ); } }, [open, selectedItem]); const handleSave = async (itemToSave: ScheduleItem) => { try { setFieldErrors(undefined); await validate(validator, itemToSave); onSave(itemToSave); } catch (error) { setFieldErrors((error as ValidationError).fieldErrors); } }; const save = async () => { await handleSave(editItem); }; const saveandactivate = async () => { await handleSave({ ...editItem, active: true }); }; const remove = () => { onSave({ ...editItem, deleted: true }); }; const DayOfWeekButton = (flag: number) => { const dayIndex = Math.log2(flag); const isSelected = (editItem.flags & flag) === flag; return ( {dow[dayIndex]} ); }; const handleClose = ( _event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown' ) => { if (reason !== 'backdropClick') { onClose(); } }; const handleScheduleTypeChange = ( _event: React.SyntheticEvent, flag: ScheduleFlag | null ) => { if (flag !== null) { setFieldErrors(undefined); // clear any validation errors setScheduleType(flag); // wipe the time field when changing the schedule type // set the flags based on type const newFlags = flag === ScheduleFlag.SCHEDULE_DAY ? FLAG_ALL_DAYS : flag; setEditItem((prev) => ({ ...prev, time: '', flags: newFlags })); } }; const handleDOWChange = ( _event: React.SyntheticEvent, flags: string[] ) => { const newFlags = getFlagDOWnumber(flags) === 0 ? FLAG_ALL_DAYS : getFlagDOWnumber(flags); setEditItem((prev) => ({ ...prev, flags: newFlags })); }; const isDaySchedule = scheduleType === ScheduleFlag.SCHEDULE_DAY; const isTimerSchedule = scheduleType === ScheduleFlag.SCHEDULE_TIMER; const isImmediateSchedule = scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE; const needsTimeField = isDaySchedule || isTimerSchedule; const dowFlags = getFlagDOWstring(editItem.flags); const timeFieldValue = needsTimeField ? editItem.time === '' ? DEFAULT_TIME : editItem.time : editItem.time === DEFAULT_TIME ? '' : editItem.time; const timeFieldLabel = (() => { if (scheduleType === ScheduleFlag.SCHEDULE_TIMER) return LL.TIMER(1); if (scheduleType === ScheduleFlag.SCHEDULE_CONDITION) return LL.CONDITION(); if (scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE) return LL.ONCHANGE(); if (scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE) return LL.IMMEDIATE(); return LL.TIME(1); })(); return ( {creating ? `${LL.ADD(1)} ${LL.NEW(0)}` : LL.EDIT()}  {LL.SCHEDULE(1)} {LL.SCHEDULE(0)} {LL.TIMER(0)} {LL.ONCHANGE()} {LL.CONDITION()} {LL.IMMEDIATE()} {isDaySchedule && ( {DAY_FLAGS.map(({ value, flag }) => ( {DayOfWeekButton(flag)} ))} )} {!isImmediateSchedule && ( <> } label={LL.ACTIVE()} /> {needsTimeField ? ( <> {isTimerSchedule && ( {LL.SCHEDULER_HELP_2()} )} ) : ( )} )} {!creating && ( )} {isImmediateSchedule && editItem.cmd !== '' && ( )} ); }; export default SchedulerDialog;