crmMailingAB - report.html - Loop over mailings
[civicrm-core.git] / js / angular-crmMailing.js
CommitLineData
030dce01 1(function (angular, $, _) {
030dce01 2
88e9e883 3 angular.module('crmMailing', [
8443385d 4 'crmUtil', 'crmAttachment', 'crmAutosave', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService'
f4f103fa 5 ]); // TODO ngSanitize, unsavedChanges
030dce01 6
7801d9b5
TO
7 // Time to wait before triggering AJAX update to recipients list
8 var RECIPIENTS_DEBOUNCE_MS = 100;
b73e0c53 9 var RECIPIENTS_PREVIEW_LIMIT = 10000;
7801d9b5 10
88e9e883 11 angular.module('crmMailing').config([
f4f103fa 12 '$routeProvider',
030dce01 13 function ($routeProvider) {
96ac27bd 14 $routeProvider.when('/mailing', {
030dce01
TO
15 template: '<div></div>',
16 controller: 'ListMailingsCtrl'
17 });
b7c3beb2
TO
18
19 var editorPaths = {
ef5d18a1
TO
20 '': '~/crmMailing/edit.html',
21 '/unified': '~/crmMailing/edit-unified.html',
22 '/unified2': '~/crmMailing/edit-unified2.html',
23 '/wizard': '~/crmMailing/edit-wizard.html'
b7c3beb2
TO
24 };
25 angular.forEach(editorPaths, function(editTemplate, pathSuffix) {
26 $routeProvider.when('/mailing/new' + pathSuffix, {
27 template: '<p>' + ts('Initializing...') + '</p>',
28 controller: 'CreateMailingCtrl',
29 resolve: {
30 selectedMail: function(crmMailingMgr) {
31 var m = crmMailingMgr.create();
32 return crmMailingMgr.save(m);
33 }
f4f103fa 34 }
b7c3beb2
TO
35 });
36 $routeProvider.when('/mailing/:id' + pathSuffix, {
37 templateUrl: editTemplate,
38 controller: 'EditMailingCtrl',
39 resolve: {
40 selectedMail: function($route, crmMailingMgr) {
41 return crmMailingMgr.get($route.current.params.id);
f2cdd789
TO
42 },
43 attachments: function($route, CrmAttachments) {
44 var attachments = new CrmAttachments(function () {
45 return {entity_table: 'civicrm_mailing', entity_id: $route.current.params.id};
46 });
47 return attachments.load();
b7c3beb2 48 }
f4f103fa 49 }
b7c3beb2 50 });
030dce01
TO
51 });
52 }
53 ]);
54
416abe87 55 angular.module('crmMailing').controller('ListMailingsCtrl', ['crmLegacy', 'crmNavigator', function ListMailingsCtrl(crmLegacy, crmNavigator) {
030dce01
TO
56 // We haven't implemented this in Angular, but some users may get clever
57 // about typing URLs, so we'll provide a redirect.
416abe87
PH
58 var new_url = crmLegacy.url('civicrm/mailing/browse/unscheduled', {reset: 1, scheduled: 'false'});
59 crmNavigator.redirect(new_url);
60 }]);
030dce01 61
b7c3beb2
TO
62 angular.module('crmMailing').controller('CreateMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location) {
63 // Transition URL "/mailing/new/foo" => "/mailing/123/foo"
64 var parts = $location.path().split('/'); // e.g. "/mailing/new" or "/mailing/123/wizard"
65 parts[2] = selectedMail.id;
66 $location.path(parts.join('/'));
67 $location.replace();
68 });
69
e08a61f9 70 angular.module('crmMailing').controller('EditMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location, crmMailingMgr, crmStatus, attachments, crmMailingPreviewMgr, crmBlocker, CrmAutosaveCtrl, $timeout) {
27e690c2 71 $scope.mailing = selectedMail;
f2cdd789 72 $scope.attachments = attachments;
27e690c2
TO
73 $scope.crmMailingConst = CRM.crmMailing;
74
5d8901af 75 var ts = $scope.ts = CRM.ts(null);
bdd3f781 76 var block = $scope.block = crmBlocker();
27e690c2 77
86c3a327
TO
78 $scope.isSubmitted = function isSubmitted() {
79 return _.size($scope.mailing.jobs) > 0;
80 };
81
58dfba8d
TO
82 // @return Promise
83 $scope.previewMailing = function previewMailing(mailing, mode) {
84 return crmMailingPreviewMgr.preview(mailing, mode);
85 };
86
87 // @return Promise
88 $scope.sendTest = function sendTest(mailing, attachments, recipient) {
89 var savePromise = crmMailingMgr.save(mailing)
90 .then(function () {
91 return attachments.save();
b7c3beb2 92 });
bdd3f781 93 return block(crmStatus({start: ts('Saving...'), success: ''}, savePromise)
58dfba8d
TO
94 .then(function () {
95 crmMailingPreviewMgr.sendTest(mailing, recipient);
bdd3f781 96 }));
58dfba8d
TO
97 };
98
705c61e9
TO
99 // @return Promise
100 $scope.submit = function submit() {
c9ae63bb
TO
101 if (block.check() || $scope.crmMailing.$invalid) {
102 return;
103 }
104
db083bf0 105 var promise = crmMailingMgr.save($scope.mailing)
86c3a327
TO
106 .then(function () {
107 // pre-condition: the mailing exists *before* saving attachments to it
108 return $scope.attachments.save();
109 })
110 .then(function () {
111 return crmMailingMgr.submit($scope.mailing);
112 })
113 .then(function () {
650a6ffc 114 $scope.leave('scheduled');
86c3a327
TO
115 })
116 ;
bdd3f781 117 return block(crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, promise));
2d36e6bc 118 };
58dfba8d 119
705c61e9
TO
120 // @return Promise
121 $scope.save = function save() {
bdd3f781 122 return block(crmStatus(null,
db083bf0
TO
123 crmMailingMgr
124 .save($scope.mailing)
125 .then(function () {
126 // pre-condition: the mailing exists *before* saving attachments to it
127 return $scope.attachments.save();
128 })
bdd3f781 129 ));
2d36e6bc 130 };
58dfba8d 131
705c61e9
TO
132 // @return Promise
133 $scope.delete = function cancel() {
bdd3f781 134 return block(crmStatus({start: ts('Deleting...'), success: ts('Deleted')},
f286acec 135 crmMailingMgr.delete($scope.mailing)
b0797ac3 136 .then(function () {
650a6ffc 137 $scope.leave('unscheduled');
b0797ac3 138 })
bdd3f781 139 ));
2d36e6bc 140 };
58dfba8d 141
b0797ac3 142 // @param string listingScreen 'archive', 'scheduled', 'unscheduled'
650a6ffc 143 $scope.leave = function leave(listingScreen) {
b0797ac3
TO
144 switch (listingScreen) {
145 case 'archive':
146 window.location = CRM.url('civicrm/mailing/browse/archived', {
147 reset: 1
148 });
149 break;
150 case 'scheduled':
151 window.location = CRM.url('civicrm/mailing/browse/scheduled', {
152 reset: 1,
153 scheduled: 'true'
154 });
155 break;
156 case 'unscheduled':
8acf0123 157 /* falls through */
b0797ac3
TO
158 default:
159 window.location = CRM.url('civicrm/mailing/browse/unscheduled', {
160 reset: 1,
161 scheduled: 'false'
162 });
163 }
650a6ffc 164 };
e08a61f9
TO
165
166 var myAutosave = new CrmAutosaveCtrl({
167 save: $scope.save,
168 saveIf: function() {
169 return true;
170 },
171 model: function() {
172 return [$scope.mailing, $scope.attachments.getAutosaveSignature()];
173 },
174 form: function() {
175 return $scope.crmMailing;
176 }
177 });
178 $timeout(myAutosave.start);
179 $scope.$on('$destroy', myAutosave.stop);
030dce01
TO
180 });
181
7801d9b5
TO
182 // Controller for the edit-recipients fields (
183 // WISHLIST: Move most of this to a (cache-enabled) service
184 // Scope members:
185 // - [input] mailing: object
186 // - [output] recipients: array of recipient records
08baf3ec 187 angular.module('crmMailing').controller('EditRecipCtrl', function EditRecipCtrl($scope, dialogService, crmApi, crmMailingMgr, $q, crmMetadata) {
5d8901af 188 var ts = $scope.ts = CRM.ts(null);
7801d9b5
TO
189 $scope.recipients = null;
190 $scope.getRecipientsEstimate = function () {
191 var ts = $scope.ts;
8acf0123 192 if ($scope.recipients === null) {
7801d9b5 193 return ts('(Estimating)');
f4f103fa 194 }
8acf0123 195 if ($scope.recipients.length === 0) {
7801d9b5 196 return ts('No recipients');
f4f103fa 197 }
8acf0123 198 if ($scope.recipients.length === 1) {
7801d9b5 199 return ts('~1 recipient');
f4f103fa
TO
200 }
201 if (RECIPIENTS_PREVIEW_LIMIT > 0 && $scope.recipients.length >= RECIPIENTS_PREVIEW_LIMIT) {
b73e0c53 202 return ts('>%1 recipients', {1: RECIPIENTS_PREVIEW_LIMIT});
f4f103fa 203 }
7801d9b5
TO
204 return ts('~%1 recipients', {1: $scope.recipients.length});
205 };
f4f103fa 206 $scope.getIncludesAsString = function () {
47bacc20
TO
207 var first = true;
208 var names = '';
9cd4f489 209 _.each($scope.mailing.recipients.groups.include, function (id) {
f4f103fa
TO
210 if (!first) {
211 names = names + ', ';
212 }
213 var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
47bacc20
TO
214 names = names + group[0].title;
215 first = false;
216 });
9cd4f489 217 _.each($scope.mailing.recipients.mailings.include, function (id) {
f4f103fa
TO
218 if (!first) {
219 names = names + ', ';
220 }
221 var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
47bacc20
TO
222 names = names + oldMailing[0].name;
223 first = false;
224 });
225 return names;
226 };
f4f103fa 227 $scope.getExcludesAsString = function () {
47bacc20
TO
228 var first = true;
229 var names = '';
9cd4f489 230 _.each($scope.mailing.recipients.groups.exclude, function (id) {
f4f103fa
TO
231 if (!first) {
232 names = names + ', ';
233 }
234 var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
47bacc20
TO
235 names = names + group[0].title;
236 first = false;
237 });
9cd4f489 238 _.each($scope.mailing.recipients.mailings.exclude, function (id) {
f4f103fa
TO
239 if (!first) {
240 names = names + ', ';
241 }
242 var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
47bacc20
TO
243 names = names + oldMailing[0].name;
244 first = false;
245 });
246 return names;
247 };
248
7801d9b5
TO
249 // We monitor four fields -- use debounce so that changes across the
250 // four fields can settle-down before AJAX.
251 var refreshRecipients = _.debounce(function () {
252 $scope.$apply(function () {
253 $scope.recipients = null;
8dfd5110
TO
254 crmMailingMgr.previewRecipients($scope.mailing, RECIPIENTS_PREVIEW_LIMIT).then(function (recipients) {
255 $scope.recipients = recipients;
b73e0c53 256 });
7801d9b5
TO
257 });
258 }, RECIPIENTS_DEBOUNCE_MS);
9cd4f489
TO
259 $scope.$watchCollection("mailing.recipients.groups.include", refreshRecipients);
260 $scope.$watchCollection("mailing.recipients.groups.exclude", refreshRecipients);
261 $scope.$watchCollection("mailing.recipients.mailings.include", refreshRecipients);
262 $scope.$watchCollection("mailing.recipients.mailings.exclude", refreshRecipients);
7801d9b5 263
4dd19229 264 $scope.previewRecipients = function previewRecipients() {
7801d9b5
TO
265 var model = {
266 recipients: $scope.recipients
267 };
b1fc510d 268 var options = CRM.utils.adjustDialogDefaults({
7801d9b5 269 autoOpen: false,
7801d9b5
TO
270 title: ts('Preview (%1)', {
271 1: $scope.getRecipientsEstimate()
52f515c6 272 })
b1fc510d 273 });
ef5d18a1 274 dialogService.open('recipDialog', '~/crmMailing/dialog/recipients.html', model, options);
7801d9b5 275 };
b8b2e316
TO
276
277 // Open a dialog for editing the advanced recipient options.
278 $scope.editOptions = function editOptions(mailing) {
b1fc510d 279 var options = CRM.utils.adjustDialogDefaults({
b8b2e316 280 autoOpen: false,
b8b2e316 281 title: ts('Edit Options')
b1fc510d 282 });
08baf3ec
TO
283 $q.when(crmMetadata.getFields('Mailing')).then(function(fields) {
284 var model = {
285 fields: fields,
286 mailing: mailing
287 };
288 dialogService.open('previewComponentDialog', '~/crmMailing/dialog/recipientOptions.html', model, options);
289 });
b8b2e316 290 };
7801d9b5
TO
291 });
292
293 // Controller for the "Preview Recipients" dialog
294 // Note: Expects $scope.model to be an object with properties:
295 // - recipients: array of contacts
88e9e883 296 angular.module('crmMailing').controller('PreviewRecipCtrl', function ($scope) {
5d8901af 297 $scope.ts = CRM.ts(null);
7801d9b5 298 });
493eb47a 299
493eb47a
TO
300 // Controller for the "Preview Mailing" dialog
301 // Note: Expects $scope.model to be an object with properties:
302 // - "subject"
303 // - "body_html"
304 // - "body_text"
870cbdbb 305 angular.module('crmMailing').controller('PreviewMailingDialogCtrl', function PreviewMailingDialogCtrl($scope) {
5d8901af 306 $scope.ts = CRM.ts(null);
493eb47a
TO
307 });
308
b8b2e316 309 // Controller for the "Recipients: Edit Options" dialog
3cf58cc3
TO
310 // Note: Expects $scope.model to be an object with properties:
311 // - "mailing" (APIv3 mailing object)
312 // - "fields" (list of fields)
b8b2e316
TO
313 angular.module('crmMailing').controller('EditRecipOptionsDialogCtrl', function EditRecipOptionsDialogCtrl($scope) {
314 $scope.ts = CRM.ts(null);
315 });
316
47bacc20
TO
317 // Controller for the "Preview Mailing Component" segment
318 // which displays header/footer/auto-responder
d376f33e 319 angular.module('crmMailing').controller('PreviewComponentCtrl', function PreviewComponentCtrl($scope, dialogService) {
5d8901af 320 var ts = $scope.ts = CRM.ts(null);
52f515c6 321
47bacc20 322 $scope.previewComponent = function previewComponent(title, componentId) {
f4f103fa 323 var component = _.where(CRM.crmMailing.headerfooterList, {id: "" + componentId});
47bacc20
TO
324 if (!component || !component[0]) {
325 CRM.alert(ts('Invalid component ID (%1)', {
326 1: componentId
327 }));
328 return;
329 }
b1fc510d 330 var options = CRM.utils.adjustDialogDefaults({
47bacc20 331 autoOpen: false,
47bacc20 332 title: title // component[0].name
b1fc510d 333 });
ef5d18a1 334 dialogService.open('previewComponentDialog', '~/crmMailing/dialog/previewComponent.html', component[0], options);
47bacc20
TO
335 };
336 });
337
d376f33e 338 // Controller for the "Preview Mailing Component" dialog
47bacc20
TO
339 // Note: Expects $scope.model to be an object with properties:
340 // - "name"
341 // - "subject"
342 // - "body_html"
343 // - "body_text"
d376f33e 344 angular.module('crmMailing').controller('PreviewComponentDialogCtrl', function PreviewComponentDialogCtrl($scope) {
5d8901af 345 $scope.ts = CRM.ts(null);
47bacc20
TO
346 });
347
744bebee 348 // Controller for the in-place msg-template management
870cbdbb 349 angular.module('crmMailing').controller('MsgTemplateCtrl', function MsgTemplateCtrl($scope, crmMsgTemplates, dialogService) {
5d8901af 350 var ts = $scope.ts = CRM.ts(null);
744bebee
TO
351 $scope.crmMsgTemplates = crmMsgTemplates;
352
353 // @return Promise MessageTemplate (per APIv3)
74263d6b 354 $scope.saveTemplate = function saveTemplate(mailing) {
744bebee 355 var model = {
74263d6b 356 selected_id: mailing.msg_template_id,
744bebee
TO
357 tpl: {
358 msg_title: '',
74263d6b
TO
359 msg_subject: mailing.subject,
360 msg_text: mailing.body_text,
361 msg_html: mailing.body_html
744bebee
TO
362 }
363 };
b1fc510d 364 var options = CRM.utils.adjustDialogDefaults({
744bebee 365 autoOpen: false,
744bebee 366 title: ts('Save Template')
b1fc510d 367 });
ef5d18a1 368 return dialogService.open('saveTemplateDialog', '~/crmMailing/dialog/saveTemplate.html', model, options)
f4f103fa 369 .then(function (item) {
74263d6b 370 mailing.msg_template_id = item.id;
744bebee
TO
371 return item;
372 });
373 };
374
375 // @param int id
376 // @return Promise
74263d6b 377 $scope.loadTemplate = function loadTemplate(mailing, id) {
744bebee 378 return crmMsgTemplates.get(id).then(function (tpl) {
74263d6b
TO
379 mailing.subject = tpl.msg_subject;
380 mailing.body_text = tpl.msg_text;
381 mailing.body_html = tpl.msg_html;
744bebee
TO
382 });
383 };
384 });
385
386 // Controller for the "Save Message Template" dialog
387 // Scope members:
388 // - [input] "model": Object
389 // - "selected_id": int
390 // - "tpl": Object
391 // - "msg_subject": string
392 // - "msg_text": string
393 // - "msg_html": string
88e9e883 394 angular.module('crmMailing').controller('SaveMsgTemplateDialogCtrl', function SaveMsgTemplateDialogCtrl($scope, crmMsgTemplates, dialogService) {
5d8901af 395 var ts = $scope.ts = CRM.ts(null);
744bebee
TO
396 $scope.saveOpt = {mode: '', newTitle: ''};
397 $scope.selected = null;
398
399 $scope.save = function save() {
400 var tpl = _.extend({}, $scope.model.tpl);
401 switch ($scope.saveOpt.mode) {
402 case 'add':
403 tpl.msg_title = $scope.saveOpt.newTitle;
404 break;
405 case 'update':
406 tpl.id = $scope.selected.id;
407 tpl.msg_title = $scope.selected.msg_title;
408 break;
409 default:
410 throw 'SaveMsgTemplateDialogCtrl: Unrecognized mode: ' + $scope.saveOpt.mode;
411 }
412 return crmMsgTemplates.save(tpl)
413 .then(function (item) {
414 CRM.status(ts('Saved'));
415 return item;
416 });
417 };
418
419 function scopeApply(f) {
420 return function () {
421 var args = arguments;
422 $scope.$apply(function () {
423 f.apply(args);
424 });
425 };
426 }
427
428 function init() {
429 crmMsgTemplates.get($scope.model.selected_id).then(
430 function (tpl) {
431 $scope.saveOpt.mode = 'update';
432 $scope.selected = tpl;
433 },
434 function () {
435 $scope.saveOpt.mode = 'add';
436 $scope.selected = null;
437 }
438 );
439 // When using dialogService with a button bar, the major button actions
440 // need to be registered with the dialog widget (and not embedded in
441 // the body of the dialog).
269d44f5
CW
442 var buttons = [
443 {
444 text: ts('Save'),
445 icons: {primary: 'ui-icon-check'},
446 click: function () {
447 $scope.save().then(function (item) {
448 dialogService.close('saveTemplateDialog', item);
449 });
450 }
451 },
452 {
453 text: ts('Cancel'),
454 icons: {primary: 'ui-icon-close'},
455 click: function () {
456 dialogService.cancel('saveTemplateDialog');
457 }
458 }
459 ];
744bebee
TO
460 dialogService.setButtons('saveTemplateDialog', buttons);
461 }
f4f103fa 462
744bebee
TO
463 setTimeout(scopeApply(init), 0);
464 });
2d06b3b6 465
87391d86
TO
466 angular.module('crmMailing').controller('EmailAddrCtrl', function EmailAddrCtrl($scope, crmFromAddresses, crmUiAlert) {
467 var ts = CRM.ts(null);
468 function changeAlert(winnerField, loserField) {
469 crmUiAlert({
470 title: ts('Conflict'),
471 text: ts('The "%1" option conflicts with the "%2" option. The "%2" option has been disabled.', {
472 1: winnerField,
473 2: loserField
474 })
475 });
476 }
477
0a993c89 478 $scope.crmFromAddresses = crmFromAddresses;
87391d86
TO
479 $scope.checkReplyToChange = function checkReplyToChange(mailing) {
480 if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
481 mailing.override_verp = '1';
16f12592 482 changeAlert(ts('Reply-To'), ts('Track Replies'));
87391d86
TO
483 }
484 };
485 $scope.checkVerpChange = function checkVerpChange(mailing) {
486 if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
487 mailing.replyto_email = '';
488 changeAlert(ts('Track Replies'), ts('Reply-To'));
489 }
490 };
0a993c89 491 });
21fb26b8
TO
492
493 var lastEmailTokenAlert = null;
89f476c4 494 angular.module('crmMailing').controller('EmailBodyCtrl', function EmailBodyCtrl($scope, crmMailingMgr, crmUiAlert, $timeout) {
21fb26b8
TO
495 var ts = CRM.ts(null);
496
89f476c4
TO
497 // ex: if (!hasAllTokens(myMailing, 'body_text)) alert('Oh noes!');
498 $scope.hasAllTokens = function hasAllTokens(mailing, field) {
21fb26b8
TO
499 return _.isEmpty(crmMailingMgr.findMissingTokens(mailing, field));
500 };
501
89f476c4
TO
502 // ex: checkTokens(myMailing, 'body_text', 'insert:body_text')
503 // ex: checkTokens(myMailing, '*')
504 $scope.checkTokens = function checkTokens(mailing, field, insertEvent) {
21fb26b8
TO
505 if (lastEmailTokenAlert) {
506 lastEmailTokenAlert.close();
507 }
89f476c4
TO
508 var missing, insertable;
509 if (field == '*') {
510 insertable = false;
511 missing = angular.extend({},
512 crmMailingMgr.findMissingTokens(mailing, 'body_html'),
513 crmMailingMgr.findMissingTokens(mailing, 'body_text')
514 );
515 } else {
516 insertable = !_.isEmpty(insertEvent);
517 missing = crmMailingMgr.findMissingTokens(mailing, field);
518 }
519 if (!_.isEmpty(missing)) {
520 lastEmailTokenAlert = crmUiAlert({
521 type: 'error',
522 title: ts('Required tokens'),
523 templateUrl: '~/crmMailing/dialog/tokenAlert.html',
524 scope: angular.extend($scope.$new(), {
525 insertable: insertable,
526 insertToken: function(token) {
527 $timeout(function(){
528 $scope.$broadcast(insertEvent, '{' + token + '}');
529 $timeout(function(){
530 checkTokens(mailing, field, insertEvent);
531 });
532 });
533 },
534 missing: missing
535 })
21fb26b8 536 });
21fb26b8 537 }
d03e0d13 538 };
21fb26b8 539 });
030dce01 540})(angular, CRM.$, CRM._);