import { useCallback, useEffect, useMemo, useState } from 'react'; import { useBlocker } from 'react-router'; import { toast } from 'react-toastify'; import AddIcon from '@mui/icons-material/Add'; import CancelIcon from '@mui/icons-material/Cancel'; import CircleIcon from '@mui/icons-material/Circle'; import WarningIcon from '@mui/icons-material/Warning'; import { Box, Button, Divider, Stack, Typography } from '@mui/material'; import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; import { updateState, useRequest } from 'alova/client'; import { BlockNavigation, ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import { useInterval } from 'utils'; import { readSchedule, writeSchedule } from '../../api/app'; import SettingsSchedulerDialog from './SchedulerDialog'; import { ScheduleFlag } from './types'; import type { Schedule, ScheduleItem } from './types'; import { schedulerItemValidation } from './validators'; // Constants const INTERVAL_DELAY = 30000; // 30 seconds const MIN_ID = -100; const MAX_ID = 100; const ICON_SIZE = 16; const SCHEDULE_FLAG_THRESHOLD = 127; const REFERENCE_YEAR = 2017; const REFERENCE_MONTH = '01'; const LOG_2 = Math.log(2); // Days of week starting from Monday (1-7) const WEEK_DAYS = [1, 2, 3, 4, 5, 6, 7] as const; const DEFAULT_SCHEDULE_ITEM: Omit = { active: false, deleted: false, flags: ScheduleFlag.SCHEDULE_DAY, time: '', cmd: '', value: '', name: '' }; const scheduleTheme = { Table: ` --data-table-library_grid-template-columns: 36px 210px 100px 192px repeat(1, minmax(100px, 1fr)) 160px; `, BaseRow: ` font-size: 14px; .td { height: 32px; } `, BaseCell: ` &:nth-of-type(2) { text-align: center; } &:nth-of-type(1) { text-align: center; } `, HeaderRow: ` text-transform: uppercase; background-color: black; color: #90CAF9; .th { border-bottom: 1px solid #565656; height: 36px; } `, Row: ` background-color: #1e1e1e; position: relative; cursor: pointer; .td { border-bottom: 1px solid #565656; } &:hover .td { background-color: #177ac9; } ` }; const scheduleTypeLabels: Record = { [ScheduleFlag.SCHEDULE_IMMEDIATE]: 'Immediate', [ScheduleFlag.SCHEDULE_TIMER]: 'Timer', [ScheduleFlag.SCHEDULE_CONDITION]: 'Condition', [ScheduleFlag.SCHEDULE_ONCHANGE]: 'On Change' }; const Scheduler = () => { const { LL, locale } = useI18nContext(); const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); const [selectedScheduleItem, setSelectedScheduleItem] = useState(); const [dow, setDow] = useState([]); const [creating, setCreating] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); useLayoutTitle(LL.SCHEDULER()); const { data: schedule, send: fetchSchedule, error } = useRequest(readSchedule, { initialData: [] }); const { send: updateSchedule } = useRequest( (data: Schedule) => writeSchedule(data), { immediate: false } ); const hasScheduleChanged = useCallback((si: ScheduleItem) => { return ( si.id !== si.o_id || (si.name || '') !== (si.o_name || '') || si.active !== si.o_active || si.deleted !== si.o_deleted || si.flags !== si.o_flags || si.time !== si.o_time || si.cmd !== si.o_cmd || si.value !== si.o_value ); }, []); const intervalCallback = useCallback(() => { if (numChanges === 0) { void fetchSchedule(); } }, [numChanges, fetchSchedule]); useInterval(intervalCallback, INTERVAL_DELAY); useEffect(() => { const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }); const days = WEEK_DAYS.map((day) => { const dayStr = String(day).padStart(2, '0'); return new Date( `${REFERENCE_YEAR}-${REFERENCE_MONTH}-${dayStr}T00:00:00+00:00` ); }); setDow(days.map((date) => formatter.format(date))); }, [locale]); const schedule_theme = useTheme(scheduleTheme); const saveSchedule = useCallback(async () => { try { await updateSchedule({ schedule: schedule .filter((si: ScheduleItem) => !si.deleted) .map((condensed_si: ScheduleItem) => ({ id: condensed_si.id, active: condensed_si.active, flags: condensed_si.flags, time: condensed_si.time, cmd: condensed_si.cmd, value: condensed_si.value, name: condensed_si.name })) }); toast.success(LL.SCHEDULE_UPDATED()); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); toast.error(message); } finally { await fetchSchedule(); setNumChanges(0); } }, [LL, schedule, updateSchedule, fetchSchedule]); const editScheduleItem = useCallback((si: ScheduleItem) => { setCreating(false); setSelectedScheduleItem(si); setDialogOpen(true); if (si.o_name === undefined) { si.o_name = si.name; } }, []); const onDialogClose = useCallback(() => { setDialogOpen(false); }, []); const onDialogCancel = useCallback(async () => { await fetchSchedule().then(() => { setNumChanges(0); }); }, [fetchSchedule]); const onDialogSave = useCallback( (updatedItem: ScheduleItem) => { setDialogOpen(false); void updateState(readSchedule(), (data: ScheduleItem[]) => { const new_data = creating ? [...data, updatedItem] : data.map((si) => si.id === updatedItem.id ? { ...si, ...updatedItem } : si ); setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length); return new_data; }); }, [creating, hasScheduleChanged] ); const addScheduleItem = useCallback(() => { setCreating(true); const newItem: ScheduleItem = { id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID), ...DEFAULT_SCHEDULE_ITEM }; setSelectedScheduleItem(newItem); setDialogOpen(true); }, []); const filteredAndSortedSchedule = useMemo( () => schedule .filter((si: ScheduleItem) => !si.deleted) .sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags), [schedule] ); const dayBox = useCallback( (si: ScheduleItem, flag: number) => { const dayIndex = Math.log(flag) / LOG_2; const isActive = (si.flags & flag) === flag; return ( <> {dow[dayIndex]} ); }, [dow] ); const scheduleType = useCallback((si: ScheduleItem) => { const label = scheduleTypeLabels[si.flags]; return ( {label || ''} ); }, []); const renderSchedule = useCallback(() => { if (!schedule) { return ( ); } return ( {(tableList: ScheduleItem[]) => ( <>
{LL.SCHEDULE(0)} {LL.TIME(0)}/Cond. {LL.COMMAND(0)} {LL.VALUE(0)} {LL.NAME(0)}
{tableList.map((si: ScheduleItem) => ( editScheduleItem(si)}> {si.flags > SCHEDULE_FLAG_THRESHOLD ? ( scheduleType(si) ) : ( <> {dayBox(si, ScheduleFlag.SCHEDULE_MON)} {dayBox(si, ScheduleFlag.SCHEDULE_TUE)} {dayBox(si, ScheduleFlag.SCHEDULE_WED)} {dayBox(si, ScheduleFlag.SCHEDULE_THU)} {dayBox(si, ScheduleFlag.SCHEDULE_FRI)} {dayBox(si, ScheduleFlag.SCHEDULE_SAT)} {dayBox(si, ScheduleFlag.SCHEDULE_SUN)} )} {si.time} {si.cmd} {si.value} {si.name} ))} )}
); }, [ schedule, error, fetchSchedule, filteredAndSortedSchedule, schedule_theme, editScheduleItem, LL, dayBox, scheduleType ]); return ( {blocker ? : null} {LL.SCHEDULER_HELP_1()}. {renderSchedule()} {selectedScheduleItem && ( )} {numChanges !== 0 && ( )} ); }; export default Scheduler;