99 />
1010 <base-input
1111 v-model =" htmlMessage"
12- :placeholder =" $t('Enter email message')"
12+ :placeholder ="
13+ $t('Enter email message (use {{placeholder}} for CSV data)')
14+ "
1315 text-area
1416 rows =" 5"
1517 class =" mb-2"
1618 />
17- <base-input
18- v-model =" emailList"
19- :placeholder =" $t('Enter recipient emails, one per line')"
20- text-area
21- rows =" 5"
22- class =" mb-2"
23- />
24- <!-- Display invalid emails if any -->
25- <div v-if =" invalidEmailsList.length > 0" class =" mt-2" >
26- <p class =" text-red-500" >
27- {{ $t('The following emails were invalid and have been removed:') }}
28- </p >
29- <ul class =" list-disc list-inside text-red-500" >
30- <li v-for =" email in invalidEmailsList" :key =" email" >{{ email }}</li >
31- </ul >
19+
20+ <!-- Toggle between Email List and CSV File -->
21+ <div class =" mb-2" >
22+ <label >
23+ <input v-model =" inputMethod" type =" radio" value =" emailList" />
24+ {{ $t('Enter Email List') }}
25+ </label >
26+ <label class =" ml-4" >
27+ <input v-model =" inputMethod" type =" radio" value =" csvFile" />
28+ {{ $t('Upload CSV File') }}
29+ </label >
30+ </div >
31+
32+ <!-- Email List Input -->
33+ <div v-if =" inputMethod === 'emailList'" >
34+ <base-input
35+ v-model =" emailList"
36+ :placeholder =" $t('Enter recipient emails, one per line')"
37+ text-area
38+ rows =" 5"
39+ class =" mb-2"
40+ />
41+ <!-- Display invalid emails if any -->
42+ <div v-if =" invalidEmailsList.length > 0" class =" mt-2" >
43+ <p class =" text-red-500" >
44+ {{ $t('The following emails were invalid and have been removed:') }}
45+ </p >
46+ <ul class =" list-disc list-inside text-red-500" >
47+ <li v-for =" email in invalidEmailsList" :key =" email" >{{ email }}</li >
48+ </ul >
49+ </div >
50+ </div >
51+
52+ <!-- CSV File Upload -->
53+ <div v-else-if =" inputMethod === 'csvFile'" >
54+ <input
55+ type =" file"
56+ accept =" .csv"
57+ class =" mb-2"
58+ @change =" handleFileUpload"
59+ />
60+ <!-- Display CSV parsing errors if any -->
61+ <div v-if =" csvErrors.length > 0" class =" mt-2 text-red-500" >
62+ <p >{{ $t('Errors in CSV file:') }}</p >
63+ <ul class =" list-disc list-inside" >
64+ <li v-for =" error in csvErrors" :key =" error" >{{ error }}</li >
65+ </ul >
66+ </div >
3267 </div >
68+
3369 <div class =" flex gap-2" >
3470 <base-button
3571 :action =" sendEmails"
4783 :text =" $t('actions.show_preview')"
4884 :alt =" $t('actions.show_preview')"
4985 />
86+ <base-button
87+ type =" bare"
88+ class =" px-2 py-1 mt-4"
89+ variant =" outline"
90+ :action =" showAdminEmailAssets"
91+ :text =" $t('~~Show Email Images')"
92+ :alt =" $t('~~Show Email Images')"
93+ />
5094 </div >
95+
96+ <!-- Task Status Display -->
5197 <div v-if =" taskStatus" class =" mt-4" >
5298 <p >{{ $t('Task Status') }}: {{ taskStatus.state }}</p >
5399 <div v-if =" taskStatus.state === 'SUCCESS'" >
54- <p >{{ $t('Emails sent successfully.') }}</p >
100+ <p >{{ $t('Email sending process completed.') }}</p >
101+ <!-- Display sent emails -->
102+ <div v-if =" sentEmails.length > 0" class =" mt-2" >
103+ <p class =" text-green-600" >{{ $t('Successfully sent to:') }}</p >
104+ <ul class =" list-disc list-inside text-green-600" >
105+ <li v-for =" email in sentEmails" :key =" email" >{{ email }}</li >
106+ </ul >
107+ </div >
108+ <!-- Display unsuccessful emails -->
109+ <div v-if =" unsuccessfulEmails.length > 0" class =" mt-2" >
110+ <p class =" text-red-500" >{{ $t('Failed to send to:') }}</p >
111+ <ul class =" list-disc list-inside text-red-500" >
112+ <li v-for =" item in unsuccessfulEmails" :key =" item.email" >
113+ {{ item.email }}: {{ item.error }}
114+ </li >
115+ </ul >
116+ </div >
55117 </div >
56118 <div v-else-if =" taskStatus.state === 'FAILURE'" >
57119 <p >{{ $t('Failed to send emails.') }}</p >
120+ <!-- Display error details if available -->
121+ <p v-if =" taskStatus.result" >{{ taskStatus.result }}</p >
58122 </div >
59123 <div v-else >
60124 <p >{{ $t('Processing...') }}</p >
@@ -73,83 +137,116 @@ import CmsViewer from '@/components/cms/CmsViewer.vue';
73137import useDialogs from ' @/hooks/useDialogs' ;
74138import { ref } from ' vue' ;
75139import { getErrorMessage } from ' @/utils/errors' ;
140+ import AdminEmailAssets from ' @/components/admin/AdminEmailAssets.vue' ;
76141
77142const { t } = useI18n ();
78143const $toasted = useToast ();
79144const { component } = useDialogs ();
80145
81146const subject = ref (' ' );
82147const htmlMessage = ref (' ' );
148+ const inputMethod = ref (' emailList' ); // 'emailList' or 'csvFile'
83149const emailList = ref (' ' );
150+ const csvFile = ref <File | null >(null );
84151const invalidEmailsList = ref <string []>([]);
85- const taskStatus = ref (null );
152+ const csvErrors = ref <string []>([]);
153+ const taskStatus = ref <any >(null );
154+ const sentEmails = ref <string []>([]);
155+ const unsuccessfulEmails = ref <{ email: string ; error: string }[]>([]);
86156let statusInterval = null ;
87- const editor = ref <HTMLElement | null >(null );
157+
158+ const handleFileUpload = (event : Event ) => {
159+ const target = event .target as HTMLInputElement ;
160+ csvFile .value =
161+ target .files && target .files .length > 0 ? target .files [0 ] : null ;
162+ };
163+
164+ const validateEmail = (email : string ) => {
165+ const emailRegex =
166+ / ^ (([^ \s "(),. :;<>@[\\\] ] + (\. [^ \s "(),. :;<>@[\\\] ] + )* )| (". + "))@(([^ \s "(),. :;<>@[\\\] ] + \. )+ [^ \s "(),. :;<>@[\\\] ] {2,} )$ / i ;
167+ return emailRegex .test (email );
168+ };
88169
89170const sendEmails = async () => {
90- if (! subject .value || ! htmlMessage .value || ! emailList .value .trim ()) {
91- $toasted .error (
92- t (' Please fill all fields and add at least one recipient email.' ),
93- );
171+ if (! subject .value || ! htmlMessage .value ) {
172+ $toasted .error (t (' Please fill all fields and add at least one recipient.' ));
94173 return ;
95174 }
96175
97- const emails = emailList .value
98- .split (' \n ' )
99- .map ((email ) => email .trim ())
100- .filter ((email ) => email !== ' ' );
176+ const formData = new FormData ();
177+ formData .append (' subject' , subject .value );
178+ formData .append (' html_message' , htmlMessage .value );
101179
102- // Validate emails
103- const invalidEmails = emails .filter ((email ) => ! validateEmail (email ));
104- const validEmails = emails .filter ((email ) => validateEmail (email ));
180+ if (inputMethod .value === ' emailList' ) {
181+ if (! emailList .value .trim ()) {
182+ $toasted .error (t (' Please enter at least one recipient email.' ));
183+ return ;
184+ }
185+
186+ const emails = emailList .value
187+ .split (' \n ' )
188+ .map ((email ) => email .trim ())
189+ .filter ((email ) => email !== ' ' );
190+
191+ // Validate emails
192+ const invalidEmails = emails .filter ((email ) => ! validateEmail (email ));
193+ const validEmails = emails .filter ((email ) => validateEmail (email ));
194+
195+ if (invalidEmails .length > 0 ) {
196+ // Remove invalid emails from emailList
197+ emailList .value = validEmails .join (' \n ' );
198+ // Update invalidEmailsList
199+ invalidEmailsList .value = invalidEmails ;
200+ if (validEmails .length === 0 ) {
201+ $toasted .error (t (' All emails were invalid and have been removed.' ));
202+ } else {
203+ $toasted .error (
204+ t (' Invalid email addresses have been removed: ' ) +
205+ invalidEmails .join (' , ' ) +
206+ ' . ' +
207+ t (' Please click Send Emails again.' ),
208+ );
209+ }
210+ return ;
211+ }
105212
106- if (invalidEmails .length > 0 ) {
107- // Remove invalid emails from emailList
108- emailList .value = validEmails .join (' \n ' );
109- // Update invalidEmailsList
110- invalidEmailsList .value = invalidEmails ;
111213 if (validEmails .length === 0 ) {
112- $toasted .error (t (' All emails were invalid and have been removed.' ));
113- } else {
114- $toasted .error (
115- t (' Invalid email addresses have been removed: ' ) +
116- invalidEmails .join (' , ' ) +
117- ' . ' +
118- t (' Please click Send Emails again.' ),
119- );
214+ $toasted .error (t (' No valid email addresses to send.' ));
215+ return ;
120216 }
121- return ;
122- }
123217
124- if (validEmails .length === 0 ) {
125- $toasted .error (t (' No valid email addresses to send.' ));
218+ // Append emails to formData
219+ formData .append (' emails' , JSON .stringify (validEmails ));
220+ } else if (inputMethod .value === ' csvFile' ) {
221+ if (! csvFile .value ) {
222+ $toasted .error (t (' Please upload a CSV file.' ));
223+ return ;
224+ }
225+ formData .append (' csv_file' , csvFile .value );
226+ } else {
227+ $toasted .error (t (' Invalid input method.' ));
126228 return ;
127229 }
128230
129231 try {
130- const response = await axios .post (` admins/send_bulk_email ` , {
131- emails: validEmails ,
132- subject: subject . value ,
133- html_message: htmlMessage . value ,
232+ const response = await axios .post (` admins/send_bulk_email ` , formData , {
233+ headers: {
234+ ' Content-Type ' : ' multipart/form-data ' ,
235+ } ,
134236 });
135237
136238 if (response .status === 202 ) {
137239 $toasted .success (t (' Emails are being sent.' ));
138240 const taskId = response .data .task_id ;
139241 checkTaskStatus (taskId );
140242 invalidEmailsList .value = [];
243+ csvErrors .value = [];
141244 }
142245 } catch (error ) {
143246 $toasted .error (getErrorMessage (error ));
144247 }
145248};
146249
147- const validateEmail = (email : string ) => {
148- const emailRegex =
149- / ^ (([^ \s "(),. :;<>@[\\\] ] + (\. [^ \s "(),. :;<>@[\\\] ] + )* )| (". + "))@(([^ \s "(),. :;<>@[\\\] ] + \. )+ [^ \s "(),. :;<>@[\\\] ] {2,} )$ / i ;
150- return emailRegex .test (email );
151- };
152-
153250const checkTaskStatus = (taskId : string ) => {
154251 if (statusInterval ) {
155252 clearInterval (statusInterval );
@@ -160,6 +257,12 @@ const checkTaskStatus = (taskId: string) => {
160257 taskStatus .value = statusResponse .data ;
161258 if ([' SUCCESS' , ' FAILURE' ].includes (taskStatus .value .state )) {
162259 clearInterval (statusInterval );
260+
261+ // Extract sent and unsuccessful emails from the task result
262+ if (taskStatus .value .result ) {
263+ sentEmails .value = taskStatus .value .result .sent || [];
264+ unsuccessfulEmails .value = taskStatus .value .result .unsuccessful || [];
265+ }
163266 }
164267 } catch {
165268 $toasted .error (t (' Failed to check task status.' ));
@@ -180,11 +283,19 @@ const showPreview = async () => {
180283 },
181284 });
182285};
286+
287+ const showAdminEmailAssets = async () => {
288+ await component ({
289+ title: t (' Email Images' ),
290+ component: AdminEmailAssets ,
291+ classes: ' w-full h-96 overflow-auto p-3' ,
292+ modalClasses: ' bg-white max-w-3xl shadow' ,
293+ });
294+ };
183295 </script >
184296
185297<style scoped>
186- .wysiwyg-editor {
187- min-height : 150px ;
188- outline : none ;
298+ .send-bulk-email label {
299+ font-weight : normal ;
189300}
190301 </style >
0 commit comments