import useChannelsStore from '../../store/channels.jsx'; import { parseDate, RECURRING_DAY_OPTIONS, toTimeString, useDateTimeFormat, useTimeHelpers, } from '../../utils/dateTimeUtils.js'; import React, { useEffect, useMemo, useState } from 'react'; import { useForm } from '@mantine/form'; import dayjs from 'dayjs'; import { notifications } from '@mantine/notifications'; import { Badge, Button, Card, Group, Modal, MultiSelect, Select, Stack, Switch, Text, TextInput, } from '@mantine/core'; import { DatePickerInput, TimeInput } from '@mantine/dates'; import { deleteRecordingById } from '../../utils/cards/RecordingCardUtils.js'; import { deleteRecurringRuleById, getChannelOptions, getUpcomingOccurrences, updateRecurringRule, updateRecurringRuleEnabled, } from '../../utils/forms/RecurringRuleModalUtils.js'; const RecurringRuleModal = ({ opened, onClose, ruleId, recording: sourceRecording, onEditOccurrence }) => { const channels = useChannelsStore((s) => s.channels); const recurringRules = useChannelsStore((s) => s.recurringRules); const fetchRecurringRules = useChannelsStore((s) => s.fetchRecurringRules); const recordings = useChannelsStore((s) => s.recordings); const { toUserTime, userNow } = useTimeHelpers(); const { timeFormat: timeformat, dateFormat: dateformat } = useDateTimeFormat(); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [busyOccurrence, setBusyOccurrence] = useState(null); const rule = recurringRules.find((r) => r.id === ruleId); const channelOptions = useMemo(() => { return getChannelOptions(channels); }, [channels]); const form = useForm({ mode: 'controlled', initialValues: { channel_id: '', days_of_week: [], rule_name: '', start_time: dayjs().startOf('hour').format('HH:mm'), end_time: dayjs().startOf('hour').add(1, 'hour').format('HH:mm'), start_date: dayjs().toDate(), end_date: dayjs().toDate(), enabled: true, }, validate: { channel_id: (value) => (value ? null : 'Select a channel'), days_of_week: (value) => value && value.length ? null : 'Pick at least one day', end_time: (value, values) => { if (!value) return 'Select an end time'; const startValue = dayjs( values.start_time, ['HH:mm', 'hh:mm A', 'h:mm A'], true ); const endValue = dayjs(value, ['HH:mm', 'hh:mm A', 'h:mm A'], true); if ( startValue.isValid() && endValue.isValid() && endValue.diff(startValue, 'minute') === 0 ) { return 'End time must differ from start time'; } return null; }, end_date: (value, values) => { const endDate = dayjs(value); const startDate = dayjs(values.start_date); if (!value) return 'Select an end date'; if (startDate.isValid() && endDate.isBefore(startDate, 'day')) { return 'End date cannot be before start date'; } return null; }, }, }); useEffect(() => { if (opened && rule) { form.setValues({ channel_id: `${rule.channel}`, days_of_week: (rule.days_of_week || []).map((d) => String(d)), rule_name: rule.name || '', start_time: toTimeString(rule.start_time), end_time: toTimeString(rule.end_time), start_date: parseDate(rule.start_date) || dayjs().toDate(), end_date: parseDate(rule.end_date), enabled: Boolean(rule.enabled), }); } else { form.reset(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [opened, ruleId, rule]); const upcomingOccurrences = useMemo(() => { return getUpcomingOccurrences(recordings, userNow, ruleId, toUserTime); }, [recordings, ruleId, toUserTime, userNow]); const handleSave = async (values) => { if (!rule) return; setSaving(true); try { await updateRecurringRule(ruleId, values); await fetchRecurringRules(); // recordings_refreshed WS event handles recording list update notifications.show({ title: 'Recurring rule updated', message: 'Schedule adjustments saved', color: 'green', autoClose: 2500, }); onClose(); } catch (error) { console.error('Failed to update recurring rule', error); } finally { setSaving(false); } }; const handleDelete = async () => { if (!rule) return; setDeleting(true); try { await deleteRecurringRuleById(ruleId); await fetchRecurringRules(); // recordings_refreshed WS event handles recording list update notifications.show({ title: 'Recurring rule removed', message: 'All future occurrences were cancelled', color: 'red', autoClose: 2500, }); onClose(); } catch (error) { console.error('Failed to delete recurring rule', error); } finally { setDeleting(false); } }; const handleToggleEnabled = async (checked) => { if (!rule) return; setSaving(true); try { await updateRecurringRuleEnabled(ruleId, checked); await fetchRecurringRules(); // recordings_refreshed WS event handles recording list update notifications.show({ title: checked ? 'Recurring rule enabled' : 'Recurring rule paused', message: checked ? 'Future occurrences will resume' : 'Upcoming occurrences were removed', color: checked ? 'green' : 'yellow', autoClose: 2500, }); } catch (error) { console.error('Failed to toggle recurring rule', error); form.setFieldValue('enabled', !checked); } finally { setSaving(false); } }; const handleCancelOccurrence = async (occurrence) => { setBusyOccurrence(occurrence.id); try { await deleteRecordingById(occurrence.id); // recording_cancelled WS event handles recording list update notifications.show({ title: 'Occurrence cancelled', message: 'The selected airing was removed', color: 'yellow', autoClose: 2000, }); } catch (error) { console.error('Failed to cancel occurrence', error); } finally { setBusyOccurrence(null); } }; if (!rule) { return ( The recurring rule for this recording no longer exists. {sourceRecording && ( <> Would you like to delete this recording? )} ); } const handleEnableChange = (event) => { form.setFieldValue('enabled', event.currentTarget.checked); handleToggleEnabled(event.currentTarget.checked); }; const handleStartDateChange = (value) => { form.setFieldValue('start_date', value || dayjs().toDate()); }; const handleEndDateChange = (value) => { form.setFieldValue('end_date', value); }; const handleStartTimeChange = (value) => { form.setFieldValue('start_time', toTimeString(value)); }; const handleEndTimeChange = (value) => { form.setFieldValue('end_time', toTimeString(value)); }; const UpcomingList = () => { return ( {upcomingOccurrences.map((occ) => { const occStart = toUserTime(occ.start_time); const occEnd = toUserTime(occ.end_time); return ( {occStart.format(`${dateformat}, YYYY`)} {occStart.format(timeformat)} – {occEnd.format(timeformat)} ); })} ); }; return ( {channels?.[rule.channel]?.name || `Channel ${rule.channel}`}