CRM-15578 - crmMailingAB - Submit UI
[civicrm-core.git] / js / angular-crmMailing / services.js
CommitLineData
8dfd5110
TO
1(function (angular, $, _) {
2 var partialUrl = function (relPath) {
dcc7d5c9 3 return CRM.resourceUrls['civicrm'] + '/partials/crmMailing/' + relPath;
8dfd5110
TO
4 };
5
43102e47
TO
6 // FIXME: surely there's already some helper which can do this in one line?
7 // @return string "YYYY-MM-DD hh:mm:ss"
8 var createNow = function () {
9 var currentdate = new Date();
10 var yyyy = currentdate.getFullYear();
11 var mm = currentdate.getMonth() + 1;
12 mm = mm < 10 ? '0' + mm : mm;
13 var dd = currentdate.getDate();
14 dd = dd < 10 ? '0' + dd : dd;
15 var hh = currentdate.getHours();
16 hh = hh < 10 ? '0' + hh : hh;
17 var min = currentdate.getMinutes();
18 min = min < 10 ? '0' + min : min;
19 var sec = currentdate.getSeconds();
20 sec = sec < 10 ? '0' + sec : sec;
21 return yyyy + "-" + mm + "-" + dd + " " + hh + ":" + min + ":" + sec;
22 };
23
a0214785
TO
24 // The representation of from/reply-to addresses is inconsistent in the mailing data-model,
25 // so the UI must do some adaptation. The crmFromAddresses provides a richer way to slice/dice
26 // the available "From:" addrs. Records are like the underlying OptionValues -- but add "email"
27 // and "author".
88e9e883 28 angular.module('crmMailing').factory('crmFromAddresses', function ($q, crmApi) {
a0214785 29 var emailRegex = /^"(.*)" \<([^@\>]*@[^@\>]*)\>$/;
f4f103fa 30 var addrs = _.map(CRM.crmMailing.fromAddress, function (addr) {
a0214785 31 var match = emailRegex.exec(addr.label);
86c3a327 32 return angular.extend({}, addr, {
a0214785
TO
33 email: match ? match[2] : '(INVALID)',
34 author: match ? match[1] : '(INVALID)'
35 });
36 });
f4f103fa 37
a0214785
TO
38 function first(array) {
39 return (array.length == 0) ? null : array[0];
52f515c6 40 }
a0214785
TO
41
42 return {
43 getAll: function getAll() {
44 return addrs;
45 },
46 getByAuthorEmail: function getByAuthorEmail(author, email, autocreate) {
47 var result = null;
f4f103fa 48 _.each(addrs, function (addr) {
a0214785
TO
49 if (addr.author == author && addr.email == email) {
50 result = addr;
51 }
52 });
53 if (!result && autocreate) {
54 result = {
55 label: '(INVALID) "' + author + '" <' + email + '>',
56 author: author,
57 email: email
58 };
59 addrs.push(result);
60 }
61 return result;
62 },
63 getByEmail: function getByEmail(email) {
64 return first(_.where(addrs, {email: email}));
65 },
f4f103fa 66 getByLabel: function (label) {
a0214785
TO
67 return first(_.where(addrs, {label: label}));
68 },
69 getDefault: function getDefault() {
70 return first(_.where(addrs, {is_default: "1"}));
71 }
72 };
73 });
74
88e9e883 75 angular.module('crmMailing').factory('crmMsgTemplates', function ($q, crmApi) {
f4f103fa 76 var tpls = _.map(CRM.crmMailing.mesTemplate, function (tpl) {
86c3a327 77 return angular.extend({}, tpl, {
744bebee
TO
78 //id: tpl parseInt(tpl.id)
79 });
80 });
81 window.tpls = tpls;
82 var lastModifiedTpl = null;
83 return {
84 // @return Promise MessageTemplate (per APIv3)
85 get: function get(id) {
f4f103fa 86 id = '' + id; // parseInt(id);
744bebee
TO
87 var dfr = $q.defer();
88 var tpl = _.where(tpls, {id: id});
89 if (id && tpl && tpl[0]) {
90 dfr.resolve(tpl[0]);
f4f103fa
TO
91 }
92 else {
744bebee
TO
93 dfr.reject(id);
94 }
95 return dfr.promise;
96 },
97 // Save a template
98 // @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
99 // @return Promise MessageTemplate (per APIv3)
f4f103fa
TO
100 save: function (tpl) {
101 return crmApi('MessageTemplate', 'create', tpl).then(function (response) {
744bebee 102 if (!tpl.id) {
f4f103fa 103 tpl.id = '' + response.id; //parseInt(response.id);
744bebee
TO
104 tpls.push(tpl);
105 }
106 lastModifiedTpl = tpl
107 return tpl;
108 });
109 },
110 // @return Object MessageTemplate (per APIv3)
f4f103fa 111 getLastModifiedTpl: function () {
744bebee
TO
112 return lastModifiedTpl;
113 },
114 getAll: function getAll() {
115 return tpls;
116 }
117 };
118 });
119
a0214785 120 // The crmMailingMgr service provides business logic for loading, saving, previewing, etc
88e9e883 121 angular.module('crmMailing').factory('crmMailingMgr', function ($q, crmApi, crmFromAddresses) {
4dd19229 122 var pickDefaultMailComponent = function pickDefaultMailComponent(type) {
8dfd5110 123 var mcs = _.where(CRM.crmMailing.headerfooterList, {
f4f103fa 124 component_type: type,
8dfd5110
TO
125 is_default: "1"
126 });
127 return (mcs.length >= 1) ? mcs[0].id : null;
128 };
129
130 return {
131 // @param scalar idExpr a number or the literal string 'new'
132 // @return Promise|Object Mailing (per APIv3)
4dd19229 133 getOrCreate: function getOrCreate(idExpr) {
8dfd5110
TO
134 return (idExpr == 'new') ? this.create() : this.get(idExpr);
135 },
136 // @return Promise Mailing (per APIv3)
4dd19229 137 get: function get(id) {
86c3a327
TO
138 var crmMailingMgr = this;
139 var mailing;
140 return crmApi('Mailing', 'getsingle', {id: id})
141 .then(function (getResult) {
142 mailing = getResult;
143 return $q.all([
144 crmMailingMgr._loadGroups(mailing),
145 crmMailingMgr._loadJobs(mailing)
146 ]);
147 })
148 .then(function () {
149 return mailing;
150 });
151 },
152 // Call MailingGroup.get and merge results into "mailing"
153 _loadGroups: function (mailing) {
154 return crmApi('MailingGroup', 'get', {mailing_id: mailing.id})
155 .then(function (groupResult) {
8dfd5110
TO
156 mailing.groups = {include: [], exclude: []};
157 mailing.mailings = {include: [], exclude: []};
f4f103fa 158 _.each(groupResult.values, function (mailingGroup) {
8dfd5110 159 var bucket = (mailingGroup.entity_table == 'civicrm_group') ? 'groups' : 'mailings';
89a50c67
TO
160 var entityId = parseInt(mailingGroup.entity_id);
161 mailing[bucket][mailingGroup.group_type].push(entityId);
8dfd5110 162 });
8dfd5110 163 });
86c3a327
TO
164 },
165 // Call MailingJob.get and merge results into "mailing"
166 _loadJobs: function (mailing) {
167 return crmApi('MailingJob', 'get', {mailing_id: mailing.id, is_test: 0})
168 .then(function (jobResult) {
169 mailing.jobs = mailing.jobs || {};
170 angular.extend(mailing.jobs, jobResult.values);
171 });
8dfd5110
TO
172 },
173 // @return Object Mailing (per APIv3)
4dd19229 174 create: function create() {
8dfd5110 175 return {
86c3a327 176 jobs: {}, // {jobId: JobRecord}
8dfd5110
TO
177 name: "revert this", // fixme
178 campaign_id: null,
a0214785
TO
179 from_name: crmFromAddresses.getDefault().author,
180 from_email: crmFromAddresses.getDefault().email,
8dfd5110
TO
181 replyto_email: "",
182 subject: "For {contact.display_name}", // fixme
183 dedupe_email: "1",
184 groups: {include: [2], exclude: [4]}, // fixme
185 mailings: {include: [], exclude: []},
186 body_html: "<b>Hello</b> {contact.display_name}", // fixme
187 body_text: "Hello {contact.display_name}", // fixme
188 footer_id: null, // pickDefaultMailComponent('Footer'),
189 header_id: null, // pickDefaultMailComponent('Header'),
190 visibility: "Public Pages",
191 url_tracking: "1",
192 dedupe_email: "1",
193 forward_replies: "0",
194 auto_responder: "0",
195 open_tracking: "1",
196 override_verp: "1",
197 optout_id: pickDefaultMailComponent('OptOut'),
198 reply_id: pickDefaultMailComponent('Reply'),
199 resubscribe_id: pickDefaultMailComponent('Resubscribe'),
200 unsubscribe_id: pickDefaultMailComponent('Unsubscribe')
201 };
202 },
203
705c61e9
TO
204 // @param mailing Object (per APIv3)
205 // @return Promise
f4f103fa 206 'delete': function (mailing) {
705c61e9
TO
207 if (mailing.id) {
208 return crmApi('Mailing', 'delete', {id: mailing.id});
f4f103fa
TO
209 }
210 else {
705c61e9
TO
211 var d = $q.defer();
212 d.resolve();
213 return d.promise;
214 }
215 },
216
07fa6426
TO
217 // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
218 // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
219 mergeInto: function mergeInto(mailingTgt, mailingFrom, excludes) {
220 var MAILING_FIELDS = [
70980d8e 221 // always exclude: 'id'
07fa6426
TO
222 'name',
223 'campaign_id',
224 'from_name',
225 'from_email',
226 'replyto_email',
227 'subject',
228 'dedupe_email',
229 'groups',
230 'mailings',
231 'body_html',
232 'body_text',
233 'footer_id',
234 'header_id',
235 'visibility',
236 'url_tracking',
237 'dedupe_email',
238 'forward_replies',
239 'auto_responder',
240 'open_tracking',
241 'override_verp',
242 'optout_id',
243 'reply_id',
244 'resubscribe_id',
245 'unsubscribe_id'
246 ];
247 if (!excludes) {
248 excludes = [];
249 }
250 _.each(MAILING_FIELDS, function (field) {
251 if (!_.contains(excludes, field)) {
252 mailingTgt[field] = mailingFrom[field];
253 }
254 })
255 },
256
493eb47a
TO
257 // @param mailing Object (per APIv3)
258 // @return Promise an object with "subject", "body_text", "body_html"
259 preview: function preview(mailing) {
86c3a327 260 var params = angular.extend({}, mailing, {
f4f103fa 261 options: {force_rollback: 1},
493eb47a 262 'api.Mailing.preview': {
52f515c6 263 id: '$value.id'
493eb47a
TO
264 }
265 });
f4f103fa 266 return crmApi('Mailing', 'create', params).then(function (result) {
beab9d1b 267 // changes rolled back, so we don't care about updating mailing
493eb47a
TO
268 return result.values[result.id]['api.Mailing.preview'].values;
269 });
270 },
271
8dfd5110
TO
272 // @param mailing Object (per APIv3)
273 // @param int previewLimit
274 // @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
4dd19229 275 previewRecipients: function previewRecipients(mailing, previewLimit) {
8dfd5110
TO
276 // To get list of recipients, we tentatively save the mailing and
277 // get the resulting recipients -- then rollback any changes.
86c3a327 278 var params = angular.extend({}, mailing, {
f4f103fa 279 options: {force_rollback: 1},
7575b840 280 'api.mailing_job.create': 1, // note: exact match to API default
8dfd5110
TO
281 'api.MailingRecipients.get': {
282 mailing_id: '$value.id',
58dfba8d 283 options: {limit: previewLimit},
8dfd5110
TO
284 'api.contact.getvalue': {'return': 'display_name'},
285 'api.email.getvalue': {'return': 'email'}
286 }
287 });
f4f103fa 288 return crmApi('Mailing', 'create', params).then(function (recipResult) {
beab9d1b 289 // changes rolled back, so we don't care about updating mailing
8dfd5110
TO
290 return recipResult.values[recipResult.id]['api.MailingRecipients.get'].values;
291 });
beab9d1b
TO
292 },
293
43102e47 294 // Save a (draft) mailing
705c61e9
TO
295 // @param mailing Object (per APIv3)
296 // @return Promise
f4f103fa 297 save: function (mailing) {
86c3a327 298 var params = angular.extend({}, mailing, {
7575b840
TO
299 'api.mailing_job.create': 0 // note: exact match to API default
300 });
43102e47
TO
301
302 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
303 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
304 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
305 delete params.scheduled_date;
306
86c3a327
TO
307 delete params.jobs;
308
f4f103fa
TO
309 return crmApi('Mailing', 'create', params).then(function (result) {
310 if (result.id && !mailing.id) {
311 mailing.id = result.id;
312 } // no rollback, so update mailing.id
43102e47 313 // Perhaps we should reload mailing based on result?
86c3a327 314 return mailing;
705c61e9
TO
315 });
316 },
317
318 // Schedule/send the mailing
319 // @param mailing Object (per APIv3)
320 // @return Promise
43102e47 321 submit: function (mailing) {
86c3a327 322 var crmMailingMgr = this;
6818346a
TO
323 var params = {
324 id: mailing.id,
43102e47 325 approval_date: createNow(),
6818346a 326 scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : createNow()
43102e47 327 };
86c3a327
TO
328 return crmApi('Mailing', 'submit', params)
329 .then(function (result) {
330 angular.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
331 return crmMailingMgr._loadJobs(mailing);
332 })
333 .then(function () {
334 return mailing;
335 });
705c61e9
TO
336 },
337
338 // Immediately send a test message
beab9d1b 339 // @param mailing Object (per APIv3)
58dfba8d 340 // @param to Object with either key "email" (string) or "gid" (int)
beab9d1b 341 // @return Promise for a list of delivery reports
58dfba8d 342 sendTest: function (mailing, recipient) {
86c3a327 343 var params = angular.extend({}, mailing, {
43102e47 344 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
beab9d1b
TO
345 'api.Mailing.send_test': {
346 mailing_id: '$value.id',
58dfba8d
TO
347 test_email: recipient.email,
348 test_group: recipient.gid
beab9d1b
TO
349 }
350 });
43102e47
TO
351
352 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
353 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
354 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
355 delete params.scheduled_date;
356
86c3a327
TO
357 delete params.jobs;
358
f4f103fa
TO
359 return crmApi('Mailing', 'create', params).then(function (result) {
360 if (result.id && !mailing.id) {
361 mailing.id = result.id;
362 } // no rollback, so update mailing.id
beab9d1b
TO
363 return result.values[result.id]['api.Mailing.send_test'].values;
364 });
8dfd5110
TO
365 }
366 };
367 });
58dfba8d
TO
368
369 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
370 angular.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService, crmMailingMgr, crmStatus) {
371 return {
372 // @param mode string one of 'html', 'text', or 'full'
373 // @return Promise
374 preview: function preview(mailing, mode) {
375 var templates = {
376 html: partialUrl('dialog/previewHtml.html'),
377 text: partialUrl('dialog/previewText.html'),
378 full: partialUrl('dialog/previewFull.html')
379 };
380 var result = null;
381 var p = crmMailingMgr
382 .preview(mailing)
383 .then(function (content) {
384 var options = {
385 autoOpen: false,
386 modal: true,
387 title: ts('Subject: %1', {
388 1: content.subject
389 })
390 };
391 result = dialogService.open('previewDialog', templates[mode], content, options);
392 });
393 crmStatus({start: ts('Previewing'), success: ''}, p);
394 return result;
395 },
396
397 // @param to Object with either key "email" (string) or "gid" (int)
398 // @return Promise
399 sendTest: function sendTest(mailing, recipient) {
400 var promise = crmMailingMgr.sendTest(mailing, recipient)
401 .then(function (deliveryInfos) {
402 var count = Object.keys(deliveryInfos).length;
403 if (count === 0) {
404 CRM.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
405 }
406 })
407 ;
408 return crmStatus({start: ts('Sending...'), success: ts('Sent')}, promise);
409 }
410 };
411 });
412
8dfd5110 413})(angular, CRM.$, CRM._);