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}`}
Upcoming occurrences
{upcomingOccurrences.length}
{upcomingOccurrences.length === 0 ? (
No future airings currently scheduled.
) : (
)}
);
};
export default RecurringRuleModal;