From 772dcf72365581be587456cd1f7e35c969b7c898 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 7 Feb 2026 04:59:02 +0300 Subject: [PATCH] feat: dual-channel broadcast form (Telegram + Email simultaneously) Replace single channel radio with independent toggles allowing both channels to be enabled at once with separate audiences and content. When both enabled, sends two sequential API requests. --- src/locales/en.json | 7 +- src/locales/fa.json | 7 +- src/locales/ru.json | 7 +- src/locales/zh.json | 7 +- src/pages/AdminBroadcastCreate.tsx | 704 ++++++++++++++++------------- 5 files changed, 421 insertions(+), 311 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index aad704e..4ca39d0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1099,7 +1099,12 @@ "emailContent": "Email content", "emailContentHint": "HTML supported", "emailContentPlaceholder": "

Hello, {{user_name}}!

\n

Your message here...

", - "emailVariables": "Available variables" + "emailVariables": "Available variables", + "enableTelegram": "Telegram", + "enableEmail": "Email", + "sendingBoth": "2 broadcasts will be created", + "bothCreated": "Broadcasts created", + "atLeastOneChannel": "Select at least one channel" }, "settings": { "title": "System Settings", diff --git a/src/locales/fa.json b/src/locales/fa.json index d315c08..6dc458d 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -803,7 +803,12 @@ "activity": "بر اساس فعالیت", "source": "بر اساس منبع", "tariff": "بر اساس تعرفه" - } + }, + "enableTelegram": "Telegram", + "enableEmail": "Email", + "sendingBoth": "۲ پیام‌رسانی ایجاد خواهد شد", + "bothCreated": "پیام‌رسانی‌ها ایجاد شدند", + "atLeastOneChannel": "حداقل یک کانال را انتخاب کنید" }, "payments": { "title": "تأیید پرداخت", diff --git a/src/locales/ru.json b/src/locales/ru.json index 2715923..c7faef0 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1124,7 +1124,12 @@ "emailContent": "Содержимое письма", "emailContentHint": "Поддерживается HTML", "emailContentPlaceholder": "

Здравствуйте, {{user_name}}!

\n

Текст вашего письма...

", - "emailVariables": "Доступные переменные" + "emailVariables": "Доступные переменные", + "enableTelegram": "Telegram", + "enableEmail": "Email", + "sendingBoth": "Будет создано 2 рассылки", + "bothCreated": "Рассылки созданы", + "atLeastOneChannel": "Выберите хотя бы один канал" }, "settings": { "title": "Настройки системы", diff --git a/src/locales/zh.json b/src/locales/zh.json index 5b9d533..b6435e6 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -855,7 +855,12 @@ "activity": "按活动", "source": "按来源", "tariff": "按套餐" - } + }, + "enableTelegram": "Telegram", + "enableEmail": "Email", + "sendingBoth": "将创建2个群发", + "bothCreated": "群发已创建", + "atLeastOneChannel": "请至少选择一个渠道" }, "settings": { "title": "系统设置", diff --git a/src/pages/AdminBroadcastCreate.tsx b/src/pages/AdminBroadcastCreate.tsx index 3ee1dea..6e514f8 100644 --- a/src/pages/AdminBroadcastCreate.tsx +++ b/src/pages/AdminBroadcastCreate.tsx @@ -6,7 +6,6 @@ import { adminBroadcastsApi, BroadcastFilter, TariffFilter, - BroadcastChannel, CombinedBroadcastCreateRequest, } from '../api/adminBroadcasts'; import { AdminBackButton } from '../components/admin'; @@ -118,12 +117,15 @@ export default function AdminBroadcastCreate() { const queryClient = useQueryClient(); const fileInputRef = useRef(null); - // Channel selection - const [channel, setChannel] = useState('telegram'); + // Channel toggles (both can be enabled) + const [telegramEnabled, setTelegramEnabled] = useState(true); + const [emailEnabled, setEmailEnabled] = useState(false); - // Common state - const [target, setTarget] = useState(''); - const [showFilters, setShowFilters] = useState(false); + // Separate targets per channel + const [telegramTarget, setTelegramTarget] = useState(''); + const [emailTarget, setEmailTarget] = useState(''); + const [showTelegramFilters, setShowTelegramFilters] = useState(false); + const [showEmailFilters, setShowEmailFilters] = useState(false); // Telegram-specific state const [messageText, setMessageText] = useState(''); @@ -138,29 +140,32 @@ export default function AdminBroadcastCreate() { const [emailSubject, setEmailSubject] = useState(''); const [emailContent, setEmailContent] = useState(''); + // Submitting state for dual send + const [isSubmitting, setIsSubmitting] = useState(false); + // Fetch Telegram filters const { data: filtersData, isLoading: filtersLoading } = useQuery({ queryKey: ['admin', 'broadcasts', 'filters'], queryFn: adminBroadcastsApi.getFilters, - enabled: channel === 'telegram', + enabled: telegramEnabled, }); // Fetch Email filters const { data: emailFiltersData, isLoading: emailFiltersLoading } = useQuery({ queryKey: ['admin', 'broadcasts', 'email-filters'], queryFn: adminBroadcastsApi.getEmailFilters, - enabled: channel === 'email', + enabled: emailEnabled, }); // Fetch buttons const { data: buttonsData } = useQuery({ queryKey: ['admin', 'broadcasts', 'buttons'], queryFn: adminBroadcastsApi.getButtons, - enabled: channel === 'telegram', + enabled: telegramEnabled, }); - // Preview mutations - const previewMutation = useMutation({ + // Preview mutations — separate for each channel + const telegramPreviewMutation = useMutation({ mutationFn: adminBroadcastsApi.preview, }); @@ -168,7 +173,7 @@ export default function AdminBroadcastCreate() { mutationFn: adminBroadcastsApi.previewEmail, }); - // Create mutation + // Create mutation (used for single-channel sends) const createMutation = useMutation({ mutationFn: adminBroadcastsApi.createCombined, onSuccess: (data) => { @@ -178,7 +183,7 @@ export default function AdminBroadcastCreate() { }); // Group Telegram filters - const groupedFilters = useMemo(() => { + const groupedTelegramFilters = useMemo(() => { if (!filtersData) return {}; const groups: Record = {}; @@ -215,48 +220,46 @@ export default function AdminBroadcastCreate() { return groups; }, [emailFiltersData]); - // Current filters based on channel - const currentFilters = channel === 'telegram' ? groupedFilters : groupedEmailFilters; - const isFiltersLoading = channel === 'telegram' ? filtersLoading : emailFiltersLoading; + // Selected filter info for each channel + const selectedTelegramFilter = useMemo(() => { + if (!telegramTarget || !filtersData) return null; + const all = [ + ...filtersData.filters, + ...filtersData.tariff_filters, + ...filtersData.custom_filters, + ]; + return all.find((f) => f.key === telegramTarget) ?? null; + }, [telegramTarget, filtersData]); - // Selected filter info - const selectedFilter = useMemo(() => { - if (!target) return null; + const selectedEmailFilter = useMemo(() => { + if (!emailTarget || !emailFiltersData) return null; + return emailFiltersData.filters.find((f) => f.key === emailTarget) ?? null; + }, [emailTarget, emailFiltersData]); - if (channel === 'telegram' && filtersData) { - const all = [ - ...filtersData.filters, - ...filtersData.tariff_filters, - ...filtersData.custom_filters, - ]; - return all.find((f) => f.key === target); - } + // Handle toggling channels + const handleToggleTelegram = () => { + setTelegramEnabled((prev) => !prev); + setTelegramTarget(''); + telegramPreviewMutation.reset(); + }; - if (channel === 'email' && emailFiltersData) { - return emailFiltersData.filters.find((f) => f.key === target); - } - - return null; - }, [target, channel, filtersData, emailFiltersData]); - - // Handle channel change - const handleChannelChange = (newChannel: BroadcastChannel) => { - setChannel(newChannel); - setTarget(''); - previewMutation.reset(); + const handleToggleEmail = () => { + setEmailEnabled((prev) => !prev); + setEmailTarget(''); emailPreviewMutation.reset(); }; - // Handle filter selection - const handleFilterSelect = (filterKey: string) => { - setTarget(filterKey); - setShowFilters(false); + // Handle filter selection per channel + const handleTelegramFilterSelect = (filterKey: string) => { + setTelegramTarget(filterKey); + setShowTelegramFilters(false); + telegramPreviewMutation.mutate(filterKey); + }; - if (channel === 'telegram') { - previewMutation.mutate(filterKey); - } else { - emailPreviewMutation.mutate(filterKey); - } + const handleEmailFilterSelect = (filterKey: string) => { + setEmailTarget(filterKey); + setShowEmailFilters(false); + emailPreviewMutation.mutate(filterKey); }; // Handle file selection @@ -308,53 +311,163 @@ export default function AdminBroadcastCreate() { }; // Validate form + const isTelegramValid = telegramEnabled && telegramTarget && messageText.trim().length > 0; + const isEmailValid = + emailEnabled && emailTarget && emailSubject.trim().length > 0 && emailContent.trim().length > 0; + const isValid = useMemo(() => { - if (!target) return false; + if (!telegramEnabled && !emailEnabled) return false; + if (telegramEnabled && !isTelegramValid) return false; + if (emailEnabled && !isEmailValid) return false; + return true; + }, [telegramEnabled, emailEnabled, isTelegramValid, isEmailValid]); - if (channel === 'telegram') { - return messageText.trim().length > 0; - } - - if (channel === 'email') { - return emailSubject.trim().length > 0 && emailContent.trim().length > 0; - } - - return false; - }, [target, channel, messageText, emailSubject, emailContent]); + const bothChannels = telegramEnabled && emailEnabled; // Submit - const handleSubmit = () => { + const handleSubmit = async () => { if (!isValid) return; - const data: CombinedBroadcastCreateRequest = { - channel, - target, - }; - - if (channel === 'telegram') { - data.message_text = messageText; - data.selected_buttons = selectedButtons; - + // Single channel — use existing createMutation with navigation to detail + if (telegramEnabled && !emailEnabled) { + const data: CombinedBroadcastCreateRequest = { + channel: 'telegram', + target: telegramTarget, + message_text: messageText, + selected_buttons: selectedButtons, + }; if (uploadedFileId) { - data.media = { - type: mediaType, - file_id: uploadedFileId, - }; + data.media = { type: mediaType, file_id: uploadedFileId }; } + createMutation.mutate(data); + return; } - if (channel === 'email') { - data.email_subject = emailSubject; - data.email_html_content = emailContent; + if (emailEnabled && !telegramEnabled) { + const data: CombinedBroadcastCreateRequest = { + channel: 'email', + target: emailTarget, + email_subject: emailSubject, + email_html_content: emailContent, + }; + createMutation.mutate(data); + return; } - createMutation.mutate(data); + // Both channels — two sequential requests, navigate to list + setIsSubmitting(true); + try { + const telegramData: CombinedBroadcastCreateRequest = { + channel: 'telegram', + target: telegramTarget, + message_text: messageText, + selected_buttons: selectedButtons, + }; + if (uploadedFileId) { + telegramData.media = { type: mediaType, file_id: uploadedFileId }; + } + + const emailData: CombinedBroadcastCreateRequest = { + channel: 'email', + target: emailTarget, + email_subject: emailSubject, + email_html_content: emailContent, + }; + + await adminBroadcastsApi.createCombined(telegramData); + await adminBroadcastsApi.createCombined(emailData); + + queryClient.invalidateQueries({ queryKey: ['admin', 'broadcasts'] }); + navigate('/admin/broadcasts'); + } finally { + setIsSubmitting(false); + } }; - const recipientsCount = - channel === 'telegram' - ? (previewMutation.data?.count ?? selectedFilter?.count ?? null) - : (emailPreviewMutation.data?.count ?? selectedFilter?.count ?? null); + // Recipients counts per channel + const telegramRecipientsCount = telegramEnabled + ? (telegramPreviewMutation.data?.count ?? selectedTelegramFilter?.count ?? null) + : null; + + const emailRecipientsCount = emailEnabled + ? (emailPreviewMutation.data?.count ?? selectedEmailFilter?.count ?? null) + : null; + + const isPending = createMutation.isPending || isSubmitting; + + // Render filter dropdown + const renderFilterDropdown = ( + channelType: 'telegram' | 'email', + target: string, + selectedFilter: BroadcastFilter | TariffFilter | null, + recipientsCount: number | null, + showFilters: boolean, + setShowFilters: (v: boolean) => void, + handleFilterSelect: (key: string) => void, + groupedFilters: Record, + isLoading: boolean, + ) => ( +
+ +
+ + + {showFilters && ( +
+ {isLoading ? ( +
{t('common.loading')}
+ ) : ( + Object.entries(groupedFilters).map(([group, filters]) => ( +
+
+ {FILTER_GROUP_LABEL_KEYS[group] ? t(FILTER_GROUP_LABEL_KEYS[group]) : group} +
+ {filters.map((filter) => ( + + ))} +
+ )) + )} +
+ )} +
+
+ ); return (
@@ -372,273 +485,250 @@ export default function AdminBroadcastCreate() {
- {/* Channel selection */} + {/* Channel toggles */}
+ {!telegramEnabled && !emailEnabled && ( +

{t('admin.broadcasts.atLeastOneChannel')}

+ )} + {bothChannels && ( +

{t('admin.broadcasts.sendingBoth')}

+ )}
- {/* Content */} -
- {/* Section title */} -

- {channel === 'telegram' - ? t('admin.broadcasts.telegramSection') - : t('admin.broadcasts.emailSection')} -

+ {/* Telegram section */} + {telegramEnabled && ( +
+

+ {t('admin.broadcasts.telegramSection')} +

- {/* Filter selection */} -
- -
-