From 2042274f10126344f00fc4a7d3e14fd30c53c05d Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 25 Oct 2025 09:27:31 -0500 Subject: [PATCH] Enhancement: Bulk assign custom dummy epgs. Enhancement: Display a dynamic warning dialog when saving batch channel edits that will display what will be changed. --- .../src/components/forms/ChannelBatch.jsx | 250 +++++++++++++----- 1 file changed, 181 insertions(+), 69 deletions(-) diff --git a/frontend/src/components/forms/ChannelBatch.jsx b/frontend/src/components/forms/ChannelBatch.jsx index 0527a3b6..42184f4d 100644 --- a/frontend/src/components/forms/ChannelBatch.jsx +++ b/frontend/src/components/forms/ChannelBatch.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo, useRef } from 'react'; import useChannelsStore from '../../store/channels'; import API from '../../api'; import useStreamProfilesStore from '../../store/streamProfiles'; +import useEPGsStore from '../../store/epgs'; import ChannelGroupForm from './ChannelGroup'; import { Box, @@ -53,6 +54,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { }, [ensureLogosLoaded]); const streamProfiles = useStreamProfilesStore((s) => s.profiles); + const epgs = useEPGsStore((s) => s.epgs); + const fetchEPGs = useEPGsStore((s) => s.fetchEPGs); const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false); const [selectedChannelGroup, setSelectedChannelGroup] = useState('-1'); @@ -60,6 +63,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { const [isSubmitting, setIsSubmitting] = useState(false); const [regexFind, setRegexFind] = useState(''); const [regexReplace, setRegexReplace] = useState(''); + const [selectedDummyEpgId, setSelectedDummyEpgId] = useState(null); const [groupPopoverOpened, setGroupPopoverOpened] = useState(false); const [groupFilter, setGroupFilter] = useState(''); @@ -71,10 +75,22 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { const [confirmSetNamesOpen, setConfirmSetNamesOpen] = useState(false); const [confirmSetLogosOpen, setConfirmSetLogosOpen] = useState(false); const [confirmSetTvgIdsOpen, setConfirmSetTvgIdsOpen] = useState(false); - const [confirmClearEpgsOpen, setConfirmClearEpgsOpen] = useState(false); + const [confirmBatchUpdateOpen, setConfirmBatchUpdateOpen] = useState(false); const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed); const suppressWarning = useWarningsStore((s) => s.suppressWarning); + // Fetch EPG sources when modal opens + useEffect(() => { + if (isOpen) { + fetchEPGs(); + } + }, [isOpen, fetchEPGs]); + + // Get dummy EPG sources + const dummyEpgSources = useMemo(() => { + return Object.values(epgs).filter((epg) => epg.source_type === 'dummy'); + }, [epgs]); + const form = useForm({ mode: 'uncontrolled', initialValues: { @@ -85,7 +101,88 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { }, }); + // Build confirmation message based on selected changes + const getConfirmationMessage = () => { + const changes = []; + const values = form.getValues(); + + // Check for regex name changes + if (regexFind.trim().length > 0) { + changes.push( + `• Name Change: Apply regex find "${regexFind}" replace with "${regexReplace || ''}"` + ); + } + + // Check channel group + if (selectedChannelGroup && selectedChannelGroup !== '-1') { + const groupName = channelGroups[selectedChannelGroup]?.name || 'Unknown'; + changes.push(`• Channel Group: ${groupName}`); + } + + // Check logo + if (selectedLogoId && selectedLogoId !== '-1') { + if (selectedLogoId === '0') { + changes.push(`• Logo: Use Default`); + } else { + const logoName = channelLogos[selectedLogoId]?.name || 'Selected Logo'; + changes.push(`• Logo: ${logoName}`); + } + } + + // Check stream profile + if (values.stream_profile_id && values.stream_profile_id !== '-1') { + if (values.stream_profile_id === '0') { + changes.push(`• Stream Profile: Use Default`); + } else { + const profileName = + streamProfiles[values.stream_profile_id]?.name || 'Selected Profile'; + changes.push(`• Stream Profile: ${profileName}`); + } + } + + // Check user level + if (values.user_level && values.user_level !== '-1') { + const userLevelLabel = + USER_LEVEL_LABELS[values.user_level] || values.user_level; + changes.push(`• User Level: ${userLevelLabel}`); + } + + // Check dummy EPG + if (selectedDummyEpgId) { + if (selectedDummyEpgId === 'clear') { + changes.push(`• EPG: Clear Assignment (use default dummy)`); + } else { + const epgName = epgs[selectedDummyEpgId]?.name || 'Selected EPG'; + changes.push(`• Dummy EPG: ${epgName}`); + } + } + + return changes; + }; + + const handleSubmit = () => { + const changes = getConfirmationMessage(); + + // If no changes detected, show notification + if (changes.length === 0) { + notifications.show({ + title: 'No Changes', + message: 'Please select at least one field to update.', + color: 'orange', + }); + return; + } + + // Skip warning if suppressed + if (isWarningSuppressed('batch-update-channels')) { + return onSubmit(); + } + + setConfirmBatchUpdateOpen(true); + }; + const onSubmit = async () => { + setConfirmBatchUpdateOpen(false); setIsSubmitting(true); const values = { @@ -126,6 +223,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { try { const applyRegex = regexFind.trim().length > 0; + // First, handle standard field updates (name, group, logo, etc.) if (applyRegex) { // Build per-channel updates to apply unique names via regex let flags = 'g'; @@ -153,10 +251,37 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { }); await API.bulkUpdateChannels(updates); - } else { + } else if (Object.keys(values).length > 0) { await API.updateChannels(channelIds, values); } + // Then, handle EPG assignment if a dummy EPG was selected + if (selectedDummyEpgId) { + if (selectedDummyEpgId === 'clear') { + // Clear EPG assignments + const associations = channelIds.map((id) => ({ + channel_id: id, + epg_data_id: null, + })); + await API.batchSetEPG(associations); + } else { + // Assign the selected dummy EPG + const selectedEpg = epgs[selectedDummyEpgId]; + if ( + selectedEpg && + selectedEpg.epg_data_ids && + selectedEpg.epg_data_ids.length > 0 + ) { + const epgDataId = selectedEpg.epg_data_ids[0]; + const associations = channelIds.map((id) => ({ + channel_id: id, + epg_data_id: epgDataId, + })); + await API.batchSetEPG(associations); + } + } + } + // Refresh both the channels table data and the main channels store await Promise.all([ API.requeryChannels(), @@ -305,49 +430,6 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { } }; - const handleClearEpgs = async () => { - if (!channelIds || channelIds.length === 0) { - notifications.show({ - title: 'No Channels Selected', - message: 'No channels to update.', - color: 'orange', - }); - return; - } - - // Skip warning if suppressed - if (isWarningSuppressed('batch-clear-epgs')) { - return executeClearEpgs(); - } - - setConfirmClearEpgsOpen(true); - }; - - const executeClearEpgs = async () => { - try { - // Clear EPG assignments (set to null/dummy) using existing batchSetEPG API - const associations = channelIds.map((id) => ({ - channel_id: id, - epg_data_id: null, - })); - - await API.batchSetEPG(associations); - - // batchSetEPG already shows a notification and refreshes channels - // Close the modal - setConfirmClearEpgsOpen(false); - onClose(); - } catch (error) { - console.error('Failed to clear EPG assignments:', error); - notifications.show({ - title: 'Error', - message: 'Failed to clear EPG assignments.', - color: 'red', - }); - setConfirmClearEpgsOpen(false); - } - }; - // useEffect(() => { // // const sameStreamProfile = channels.every( // // (channel) => channel.stream_profile_id == channels[0].stream_profile_id @@ -418,7 +500,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { } styles={{ hannontent: { '--mantine-color-body': '#27272A' } }} > -
+ @@ -484,20 +566,30 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { Set TVG-IDs from EPG - -