From 36ec2fb1b02c79fc600f4634db34ab81a77d8a27 Mon Sep 17 00:00:00 2001 From: Nick Sandstrom <32273437+nick4810@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:44:55 -0800 Subject: [PATCH] Extracted component and util logic --- .../components/forms/RecurringRuleModal.jsx | 199 ++++++++---------- .../utils/forms/RecurringRuleModalUtils.js | 66 ++++++ 2 files changed, 157 insertions(+), 108 deletions(-) create mode 100644 frontend/src/utils/forms/RecurringRuleModalUtils.js diff --git a/frontend/src/components/forms/RecurringRuleModal.jsx b/frontend/src/components/forms/RecurringRuleModal.jsx index 590d4641..1fbdd549 100644 --- a/frontend/src/components/forms/RecurringRuleModal.jsx +++ b/frontend/src/components/forms/RecurringRuleModal.jsx @@ -9,10 +9,17 @@ import { import React, { useEffect, useMemo, useState } from 'react'; import { useForm } from '@mantine/form'; import dayjs from 'dayjs'; -import API from '../../api.js'; 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'; export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence }) => { const channels = useChannelsStore((s) => s.channels); @@ -30,19 +37,7 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } const rule = recurringRules.find((r) => r.id === ruleId); const channelOptions = useMemo(() => { - const list = Object.values(channels || {}); - list.sort((a, b) => { - const aNum = Number(a.channel_number) || 0; - const bNum = Number(b.channel_number) || 0; - if (aNum === bNum) { - return (a.name || '').localeCompare(b.name || ''); - } - return aNum - bNum; - }); - return list.map((item) => ({ - value: `${item.id}`, - label: item.name || `Channel ${item.id}`, - })); + return getChannelOptions(channels); }, [channels]); const form = useForm({ @@ -109,41 +104,14 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } }, [opened, ruleId, rule]); const upcomingOccurrences = useMemo(() => { - const list = Array.isArray(recordings) - ? recordings - : Object.values(recordings || {}); - const now = userNow(); - return list - .filter( - (rec) => - rec?.custom_properties?.rule?.id === ruleId && - toUserTime(rec.start_time).isAfter(now) - ) - .sort( - (a, b) => - toUserTime(a.start_time).valueOf() - - toUserTime(b.start_time).valueOf() - ); + return getUpcomingOccurrences(recordings, userNow, ruleId, toUserTime); }, [recordings, ruleId, toUserTime, userNow]); const handleSave = async (values) => { if (!rule) return; setSaving(true); try { - await API.updateRecurringRule(ruleId, { - channel: values.channel_id, - days_of_week: (values.days_of_week || []).map((d) => Number(d)), - start_time: toTimeString(values.start_time), - end_time: toTimeString(values.end_time), - start_date: values.start_date - ? dayjs(values.start_date).format('YYYY-MM-DD') - : null, - end_date: values.end_date - ? dayjs(values.end_date).format('YYYY-MM-DD') - : null, - name: values.rule_name?.trim() || '', - enabled: Boolean(values.enabled), - }); + await updateRecurringRule(ruleId, values); await Promise.all([fetchRecurringRules(), fetchRecordings()]); notifications.show({ title: 'Recurring rule updated', @@ -163,7 +131,7 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } if (!rule) return; setDeleting(true); try { - await API.deleteRecurringRule(ruleId); + await deleteRecurringRuleById(ruleId); await Promise.all([fetchRecurringRules(), fetchRecordings()]); notifications.show({ title: 'Recurring rule removed', @@ -183,7 +151,7 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } if (!rule) return; setSaving(true); try { - await API.updateRecurringRule(ruleId, { enabled: checked }); + await updateRecurringRuleEnabled(ruleId, checked); await Promise.all([fetchRecurringRules(), fetchRecordings()]); notifications.show({ title: checked ? 'Recurring rule enabled' : 'Recurring rule paused', @@ -204,7 +172,7 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } const handleCancelOccurrence = async (occurrence) => { setBusyOccurrence(occurrence.id); try { - await API.deleteRecording(occurrence.id); + await deleteRecordingById(occurrence.id); await fetchRecordings(); notifications.show({ title: 'Occurrence cancelled', @@ -227,6 +195,77 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } ); } + 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 ( { - form.setFieldValue('enabled', event.currentTarget.checked); - handleToggleEnabled(event.currentTarget.checked); - }} + onChange={handleEnableChange} label={form.values.enabled ? 'Enabled' : 'Paused'} disabled={saving} /> @@ -278,15 +314,13 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } - form.setFieldValue('start_date', value || dayjs().toDate()) - } + onChange={handleStartDateChange} valueFormat="MMM D, YYYY" /> form.setFieldValue('end_date', value)} + onChange={handleEndDateChange} valueFormat="MMM D, YYYY" minDate={form.values.start_date || undefined} /> @@ -295,9 +329,7 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } - form.setFieldValue('start_time', toTimeString(value)) - } + onChange={handleStartTimeChange} withSeconds={false} format="12" amLabel="AM" @@ -306,9 +338,7 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } - form.setFieldValue('end_time', toTimeString(value)) - } + onChange={handleEndTimeChange} withSeconds={false} format="12" amLabel="AM" @@ -341,54 +371,7 @@ export const RecurringRuleModal = ({ opened, onClose, ruleId, onEditOccurrence } No future airings currently scheduled. - ) : ( - - {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)} - - - - - - - - - ); - })} - - )} + ) : } diff --git a/frontend/src/utils/forms/RecurringRuleModalUtils.js b/frontend/src/utils/forms/RecurringRuleModalUtils.js new file mode 100644 index 00000000..1eb9194a --- /dev/null +++ b/frontend/src/utils/forms/RecurringRuleModalUtils.js @@ -0,0 +1,66 @@ +import API from '../../api.js'; +import { toTimeString } from '../dateTimeUtils.js'; +import dayjs from 'dayjs'; + +export const getChannelOptions = (channels) => { + return Object.values(channels || {}) + .sort((a, b) => { + const aNum = Number(a.channel_number) || 0; + const bNum = Number(b.channel_number) || 0; + if (aNum === bNum) { + return (a.name || '').localeCompare(b.name || ''); + } + return aNum - bNum; + }) + .map((item) => ({ + value: `${item.id}`, + label: item.name || `Channel ${item.id}`, + })); +}; + +export const getUpcomingOccurrences = ( + recordings, + userNow, + ruleId, + toUserTime +) => { + const list = Array.isArray(recordings) + ? recordings + : Object.values(recordings || {}); + const now = userNow(); + return list + .filter( + (rec) => + rec?.custom_properties?.rule?.id === ruleId && + toUserTime(rec.start_time).isAfter(now) + ) + .sort( + (a, b) => + toUserTime(a.start_time).valueOf() - toUserTime(b.start_time).valueOf() + ); +}; + +export const updateRecurringRule = async (ruleId, values) => { + await API.updateRecurringRule(ruleId, { + channel: values.channel_id, + days_of_week: (values.days_of_week || []).map((d) => Number(d)), + start_time: toTimeString(values.start_time), + end_time: toTimeString(values.end_time), + start_date: values.start_date + ? dayjs(values.start_date).format('YYYY-MM-DD') + : null, + end_date: values.end_date + ? dayjs(values.end_date).format('YYYY-MM-DD') + : null, + name: values.rule_name?.trim() || '', + enabled: Boolean(values.enabled), + }); +}; + +export const deleteRecurringRuleById = async (ruleId) => { + await API.deleteRecurringRule(ruleId); +}; + +export const updateRecurringRuleEnabled = async (ruleId, checked) => { + await API.updateRecurringRule(ruleId, { enabled: checked }); +}; \ No newline at end of file