diff --git a/src/pages/AdminBroadcasts.tsx b/src/pages/AdminBroadcasts.tsx index 3928233..cbb54b3 100644 --- a/src/pages/AdminBroadcasts.tsx +++ b/src/pages/AdminBroadcasts.tsx @@ -7,7 +7,8 @@ import { Broadcast, BroadcastFilter, TariffFilter, - BroadcastCreateRequest, + BroadcastChannel, + CombinedBroadcastCreateRequest, } from '../api/adminBroadcasts'; // Icons @@ -111,6 +112,22 @@ const ChevronDownIcon = () => ( ); +const TelegramIcon = () => ( + + + +); + +const EmailIcon = () => ( + + + +); + // Status badge component const statusConfig: Record = { queued: { @@ -156,6 +173,38 @@ function StatusBadge({ status }: { status: string }) { ); } +// Channel badge component +function ChannelBadge({ channel }: { channel?: BroadcastChannel }) { + const { t } = useTranslation(); + + if (!channel || channel === 'telegram') { + return ( + + + Telegram + + ); + } + + if (channel === 'email') { + return ( + + + Email + + ); + } + + return ( + + + + + + {t('admin.broadcasts.channel.both')} + + ); +} + // Filter labels (labelKey pattern: store i18n keys, resolve at render time with t()) const FILTER_GROUP_LABEL_KEYS: Record = { basic: 'admin.broadcasts.filterGroups.basic', @@ -165,6 +214,7 @@ const FILTER_GROUP_LABEL_KEYS: Record = { activity: 'admin.broadcasts.filterGroups.activity', source: 'admin.broadcasts.filterGroups.source', tariff: 'admin.broadcasts.filterGroups.tariff', + email: 'admin.broadcasts.filterGroups.email', }; // Create broadcast modal @@ -178,7 +228,14 @@ function CreateBroadcastModal({ onClose, onSuccess }: CreateModalProps) { const queryClient = useQueryClient(); const fileInputRef = useRef(null); + // Channel selection + const [channel, setChannel] = useState('telegram'); + + // Common fields const [target, setTarget] = useState(''); + const [emailTarget, setEmailTarget] = useState(''); + + // Telegram fields const [messageText, setMessageText] = useState(''); const [selectedButtons, setSelectedButtons] = useState(['home']); const [mediaFile, setMediaFile] = useState(null); @@ -186,28 +243,48 @@ function CreateBroadcastModal({ onClose, onSuccess }: CreateModalProps) { const [mediaPreview, setMediaPreview] = useState(null); const [uploadedFileId, setUploadedFileId] = useState(null); const [isUploading, setIsUploading] = useState(false); - const [showFilters, setShowFilters] = useState(false); - // Fetch filters + // Email fields + const [emailSubject, setEmailSubject] = useState(''); + const [emailHtmlContent, setEmailHtmlContent] = useState(''); + + // UI state + const [showFilters, setShowFilters] = useState(false); + const [showEmailFilters, setShowEmailFilters] = useState(false); + + // Fetch Telegram filters const { data: filtersData, isLoading: filtersLoading } = useQuery({ queryKey: ['admin', 'broadcasts', 'filters'], queryFn: adminBroadcastsApi.getFilters, + enabled: channel === 'telegram' || channel === 'both', + }); + + // Fetch Email filters + const { data: emailFiltersData, isLoading: emailFiltersLoading } = useQuery({ + queryKey: ['admin', 'broadcasts', 'email-filters'], + queryFn: adminBroadcastsApi.getEmailFilters, + enabled: channel === 'email' || channel === 'both', }); // Fetch buttons const { data: buttonsData } = useQuery({ queryKey: ['admin', 'broadcasts', 'buttons'], queryFn: adminBroadcastsApi.getButtons, + enabled: channel === 'telegram' || channel === 'both', }); - // Preview mutation + // Preview mutations const previewMutation = useMutation({ mutationFn: adminBroadcastsApi.preview, }); + const previewEmailMutation = useMutation({ + mutationFn: adminBroadcastsApi.previewEmail, + }); + // Create mutation const createMutation = useMutation({ - mutationFn: adminBroadcastsApi.create, + mutationFn: adminBroadcastsApi.createCombined, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin', 'broadcasts'] }); onSuccess(); @@ -242,6 +319,20 @@ function CreateBroadcastModal({ onClose, onSuccess }: CreateModalProps) { return groups; }, [filtersData]); + // Group email filters + const groupedEmailFilters = useMemo(() => { + if (!emailFiltersData) return {}; + const groups: Record = {}; + + emailFiltersData.filters.forEach((f) => { + const group = f.group || 'email'; + if (!groups[group]) groups[group] = []; + groups[group].push(f); + }); + + return groups; + }, [emailFiltersData]); + // Selected filter info const selectedFilter = useMemo(() => { if (!target || !filtersData) return null; @@ -253,6 +344,11 @@ function CreateBroadcastModal({ onClose, onSuccess }: CreateModalProps) { return all.find((f) => f.key === target); }, [target, filtersData]); + const selectedEmailFilter = useMemo(() => { + if (!emailTarget || !emailFiltersData) return null; + return emailFiltersData.filters.find((f) => f.key === emailTarget); + }, [emailTarget, emailFiltersData]); + // Handle file selection const handleFileSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -303,30 +399,77 @@ function CreateBroadcastModal({ onClose, onSuccess }: CreateModalProps) { ); }; + // Validation + const canSubmit = useMemo(() => { + if (createMutation.isPending || isUploading) return false; + + if (channel === 'telegram') { + return !!target && !!messageText.trim(); + } + + if (channel === 'email') { + return !!emailTarget && !!emailSubject.trim() && !!emailHtmlContent.trim(); + } + + // Both + return ( + !!target && + !!messageText.trim() && + !!emailTarget && + !!emailSubject.trim() && + !!emailHtmlContent.trim() + ); + }, [ + channel, + target, + messageText, + emailTarget, + emailSubject, + emailHtmlContent, + createMutation.isPending, + isUploading, + ]); + // Submit const handleSubmit = () => { - if (!target || !messageText.trim()) return; + if (!canSubmit) return; - const data: BroadcastCreateRequest = { - target, - message_text: messageText, - selected_buttons: selectedButtons, + const data: CombinedBroadcastCreateRequest = { + channel, + target: channel === 'email' ? emailTarget : target, }; - if (uploadedFileId) { - data.media = { - type: mediaType, - file_id: uploadedFileId, - }; + // Telegram data + if (channel === 'telegram' || channel === 'both') { + data.message_text = messageText; + data.selected_buttons = selectedButtons; + + if (uploadedFileId) { + data.media = { + type: mediaType, + file_id: uploadedFileId, + }; + } + } + + // Email data + if (channel === 'email' || channel === 'both') { + data.email_subject = emailSubject; + data.email_html_content = emailHtmlContent; + if (channel === 'both') { + data.target = target; // Use Telegram target as primary + } } createMutation.mutate(data); }; - const recipientsCount = previewMutation.data?.count ?? selectedFilter?.count ?? null; + const telegramRecipientsCount = previewMutation.data?.count ?? selectedFilter?.count ?? null; + const emailRecipientsCount = + previewEmailMutation.data?.count ?? selectedEmailFilter?.count ?? null; return ( -
+
{/* Header */}
@@ -343,177 +486,346 @@ function CreateBroadcastModal({ onClose, onSuccess }: CreateModalProps) { {/* Content */}
- {/* Filter selection */} + {/* Channel selection */}
-
+
+ + +
+
- {showFilters && ( -
- {filtersLoading ? ( -
Loading...
- ) : ( - Object.entries(groupedFilters).map(([group, filters]) => ( -
-
- {FILTER_GROUP_LABEL_KEYS[group] - ? t(FILTER_GROUP_LABEL_KEYS[group]) - : group} -
- {filters.map((filter) => ( - - ))} -
- )) + {/* Telegram Section */} + {(channel === 'telegram' || channel === 'both') && ( +
+
+ + {t('admin.broadcasts.telegramSection')} +
+ + {/* Filter selection */} +
+ +
+ + + {showFilters && ( +
+ {filtersLoading ? ( +
Loading...
+ ) : ( + Object.entries(groupedFilters).map(([group, filters]) => ( +
+
+ {FILTER_GROUP_LABEL_KEYS[group] + ? t(FILTER_GROUP_LABEL_KEYS[group]) + : group} +
+ {filters.map((filter) => ( + + ))} +
+ )) + )} +
)}
- )} -
-
- - {/* Message text */} -
- -