diff --git a/apps/admin/app/dashboard/marketing/email/broadcast-form.tsx b/apps/admin/app/dashboard/marketing/email/broadcast-form.tsx index 466243d..7919fb9 100644 --- a/apps/admin/app/dashboard/marketing/email/broadcast-form.tsx +++ b/apps/admin/app/dashboard/marketing/email/broadcast-form.tsx @@ -1,5 +1,6 @@ 'use client'; +import { createBatchSendEmailTask, getPreSendEmailCount } from '@/services/admin/marketing'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@workspace/ui/components/button'; import { @@ -33,49 +34,42 @@ import { Textarea } from '@workspace/ui/components/textarea'; import { MarkdownEditor } from '@workspace/ui/custom-components/editor'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { Icon } from '@workspace/ui/custom-components/icon'; +import { useTranslations } from 'next-intl'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; -const emailBroadcastSchema = z.object({ - subject: z.string().min(1, 'Email subject cannot be empty'), - content: z.string().min(1, 'Email content cannot be empty'), - // Send settings - additional_emails: z - .string() - .optional() - .refine( - (value) => { - if (!value || value.trim() === '') return true; - const emails = value.split('\n').filter((email) => email.trim() !== ''); - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emails.every((email) => emailRegex.test(email.trim())); - }, - { - message: 'Please enter valid email addresses, one per line', - }, - ), - // Send time settings - scheduled_time: z.string().optional(), - user_filters: z.object({ - subscription_status: z.string().optional(), - registration_date_from: z.string().optional(), - registration_date_to: z.string().optional(), - user_groups: z.array(z.string()).default([]), - }), - rate_limit: z.object({ - email_interval_seconds: z - .number() - .min(1, 'Email interval (seconds) cannot be less than 1') - .default(1), - daily_limit: z.number().min(1, 'Daily limit must be at least 1').default(1000), - }), -}); - -type EmailBroadcastFormData = z.infer; - export default function EmailBroadcastForm() { + const t = useTranslations('marketing'); + + // Define schema with internationalized error messages + const emailBroadcastSchema = z.object({ + subject: z.string().min(1, t('subject') + ' ' + t('cannotBeEmpty')), + content: z.string().min(1, t('content') + ' ' + t('cannotBeEmpty')), + scope: z.string().default('all'), + register_start_time: z.string().optional(), + register_end_time: z.string().optional(), + additional: z + .string() + .optional() + .refine( + (value) => { + if (!value || value.trim() === '') return true; + const emails = value.split('\n').filter((email) => email.trim() !== ''); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emails.every((email) => emailRegex.test(email.trim())); + }, + { + message: t('pleaseEnterValidEmailAddresses'), + }, + ), + scheduled: z.string().optional(), + interval: z.number().min(0.1, t('emailIntervalMinimum')).optional(), + limit: z.number().min(1, t('dailyLimit')).optional(), + }); + + type EmailBroadcastFormData = z.infer; const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [estimatedRecipients, setEstimatedRecipients] = useState<{ @@ -89,68 +83,71 @@ export default function EmailBroadcastForm() { defaultValues: { subject: '', content: '', - additional_emails: '', - scheduled_time: '', - user_filters: { - subscription_status: 'all', - registration_date_from: '', - registration_date_to: '', - user_groups: [], - }, - rate_limit: { - email_interval_seconds: 1, - daily_limit: 1000, - }, + scope: 'all', + register_start_time: '', + register_end_time: '', + additional: '', + scheduled: '', + interval: 1, + limit: 1000, }, }); // Calculate recipient count - const calculateRecipients = () => { + const calculateRecipients = async () => { const formData = form.getValues(); - // Simulate user data statistics (should call API in real implementation) - let userCount = 0; + try { + // Call API to get actual recipient count + const scope = formData.scope || 'all'; - const sendingScope = formData.user_filters.subscription_status; - if (sendingScope === 'skip') { - // Send only to additional emails - userCount = 0; - } else { - let baseCount = 1500; + // Convert dates to timestamps if they exist + let register_start_time: number = 0; + let register_end_time: number = 0; - if (sendingScope === 'active') { - baseCount = Math.floor(baseCount * 0.3); // 30% active subscription users - } else if (sendingScope === 'expired') { - baseCount = Math.floor(baseCount * 0.2); // 20% expired subscription users - } else if (sendingScope === 'none') { - baseCount = Math.floor(baseCount * 0.5); // 50% no subscription users - } - // If 'all' or empty, keep baseCount unchanged (all platform users) - - // Date filter impact (simplified calculation) - if ( - formData.user_filters.registration_date_from || - formData.user_filters.registration_date_to - ) { - baseCount = Math.floor(baseCount * 0.7); // Estimate about 70% after date filtering + if (formData.register_start_time) { + register_start_time = Math.floor(new Date(formData.register_start_time).getTime()); } - userCount = baseCount; + if (formData.register_end_time) { + register_end_time = Math.floor(new Date(formData.register_end_time).getTime()); + } + + const response = await getPreSendEmailCount({ + scope, + register_start_time, + register_end_time, + }); + + const userCount = response.data?.data?.count || 0; + + // Calculate additional email count + const additionalEmails = formData.additional || ''; + const additionalCount = additionalEmails + .split('\n') + .filter((email: string) => email.trim() !== '').length; + + const total = userCount + additionalCount; + + setEstimatedRecipients({ + users: userCount, + additional: additionalCount, + total, + }); + } catch (error) { + console.error('Failed to get recipient count:', error); + // Set to 0 if API fails, don't use fallback simulation + const additionalEmails = formData.additional || ''; + const additionalCount = additionalEmails + .split('\n') + .filter((email: string) => email.trim() !== '').length; + + setEstimatedRecipients({ + users: 0, + additional: additionalCount, + total: additionalCount, + }); } - - // Calculate additional email count - const additionalEmails = formData.additional_emails || ''; - const additionalCount = additionalEmails - .split('\n') - .filter((email: string) => email.trim() !== '').length; - - const total = userCount + additionalCount; - - setEstimatedRecipients({ - users: userCount, - additional: additionalCount, - total, - }); }; // Listen to form changes @@ -158,42 +155,72 @@ export default function EmailBroadcastForm() { // Use useEffect to respond to form changes useEffect(() => { - calculateRecipients(); + const debounceTimer = setTimeout(() => { + calculateRecipients(); + }, 500); // Add debounce to avoid too frequent API calls + + return () => clearTimeout(debounceTimer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - watchedValues.user_filters?.subscription_status, - watchedValues.user_filters?.registration_date_from, - watchedValues.user_filters?.registration_date_to, - watchedValues.additional_emails, + watchedValues.scope, + watchedValues.register_start_time, + watchedValues.register_end_time, + watchedValues.additional, ]); const onSubmit = async (data: EmailBroadcastFormData) => { setLoading(true); try { // Validate scheduled send time - if (data.scheduled_time && data.scheduled_time.trim() !== '') { - const scheduledDate = new Date(data.scheduled_time); + let scheduled: number | undefined; + if (data.scheduled && data.scheduled.trim() !== '') { + const scheduledDate = new Date(data.scheduled); const now = new Date(); if (scheduledDate <= now) { - toast.error('Scheduled send time must be later than current time'); + toast.error(t('scheduledSendTimeMustBeLater')); return; } + scheduled = Math.floor(scheduledDate.getTime()); } - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 1000)); - console.log('Email broadcast data:', data); + let register_start_time: number = 0; + let register_end_time: number = 0; - if (!data.scheduled_time || data.scheduled_time.trim() === '') { - toast.success('Email sent successfully'); + if (data.register_start_time) { + register_start_time = Math.floor(new Date(data.register_start_time).getTime()); + } + + if (data.register_end_time) { + register_end_time = Math.floor(new Date(data.register_end_time).getTime()); + } + + // Prepare API request data + const requestData: API.CreateBatchSendEmailTaskRequest = { + subject: data.subject, + content: data.content, + scope: data.scope, + register_start_time, + register_end_time, + additional: data.additional || undefined, + scheduled, + interval: data.interval ? data.interval * 1000 : undefined, // Convert seconds to milliseconds + limit: data.limit, + }; + + // Call API to create batch send email task + await createBatchSendEmailTask(requestData); + + if (!data.scheduled || data.scheduled.trim() === '') { + toast.success(t('emailBroadcastTaskCreatedSuccessfully')); } else { - toast.success('Email added to scheduled send queue'); + toast.success(t('emailAddedToScheduledQueue')); } form.reset(); setOpen(false); } catch (error) { - toast.error('Send failed, please try again'); + console.error('Email broadcast failed:', error); + toast.error(t('sendFailed')); } finally { setLoading(false); } @@ -208,8 +235,10 @@ export default function EmailBroadcastForm() {
-

Email Broadcast

-

Create new email broadcast campaign

+

{t('emailBroadcast')}

+

+ {t('createNewEmailBroadcastCampaign')} +

@@ -217,7 +246,7 @@ export default function EmailBroadcastForm() { - Create Email Broadcast + {t('createBroadcast')}
@@ -228,8 +257,8 @@ export default function EmailBroadcastForm() { > - Email Content - Send Settings + {t('content')} + {t('sendSettings')} {/* Email Content Tab */} @@ -238,9 +267,12 @@ export default function EmailBroadcastForm() { name='subject' render={({ field }) => ( - Email Subject + {t('subject')} - + @@ -252,7 +284,7 @@ export default function EmailBroadcastForm() { name='content' render={({ field }) => ( - Email Content + {t('content')} - - Use Markdown editor to write email content with preview functionality - + {t('useMarkdownEditor')} )} @@ -276,32 +306,27 @@ export default function EmailBroadcastForm() {
( - Send Scope + {t('sendScope')} - - Choose the user scope for email sending. Select “Additional emails - only” to send only to the email addresses filled below - + {t('sendScopeDescription')} )} /> @@ -309,12 +334,12 @@ export default function EmailBroadcastForm() { {/* Estimated recipients info */}
- Estimated recipients: + {t('estimatedRecipients')}: {estimatedRecipients.total} - (users: {estimatedRecipients.users}, additional:{' '} + ({t('users')}: {estimatedRecipients.users}, {t('additional')}:{' '} {estimatedRecipients.additional})
@@ -324,41 +349,37 @@ export default function EmailBroadcastForm() {
( - Registration Start Date + {t('registrationStartDate')} - - Include users registered on or after this date - + {t('includeUsersRegisteredAfter')} )} /> ( - Registration End Date + {t('registrationEndDate')} - - Include users registered on or before this date - + {t('includeUsersRegisteredBefore')} )} /> @@ -367,21 +388,18 @@ export default function EmailBroadcastForm() { {/* Additional recipients */} ( - Additional Recipient Emails + {t('additionalRecipientEmails')}