crmMailingAB - report.html - Loop over mailings
[civicrm-core.git] / js / angular-crmMailing.js
1 (function (angular, $, _) {
2
3 angular.module('crmMailing', [
4 'crmUtil', 'crmAttachment', 'crmAutosave', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService'
5 ]); // TODO ngSanitize, unsavedChanges
6
7 // Time to wait before triggering AJAX update to recipients list
8 var RECIPIENTS_DEBOUNCE_MS = 100;
9 var RECIPIENTS_PREVIEW_LIMIT = 10000;
10
11 angular.module('crmMailing').config([
12 '$routeProvider',
13 function ($routeProvider) {
14 $routeProvider.when('/mailing', {
15 template: '<div></div>',
16 controller: 'ListMailingsCtrl'
17 });
18
19 var editorPaths = {
20 '': '~/crmMailing/edit.html',
21 '/unified': '~/crmMailing/edit-unified.html',
22 '/unified2': '~/crmMailing/edit-unified2.html',
23 '/wizard': '~/crmMailing/edit-wizard.html'
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 }
34 }
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);
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();
48 }
49 }
50 });
51 });
52 }
53 ]);
54
55 angular.module('crmMailing').controller('ListMailingsCtrl', ['crmLegacy', 'crmNavigator', function ListMailingsCtrl(crmLegacy, crmNavigator) {
56 // We haven't implemented this in Angular, but some users may get clever
57 // about typing URLs, so we'll provide a redirect.
58 var new_url = crmLegacy.url('civicrm/mailing/browse/unscheduled', {reset: 1, scheduled: 'false'});
59 crmNavigator.redirect(new_url);
60 }]);
61
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
70 angular.module('crmMailing').controller('EditMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location, crmMailingMgr, crmStatus, attachments, crmMailingPreviewMgr, crmBlocker, CrmAutosaveCtrl, $timeout) {
71 $scope.mailing = selectedMail;
72 $scope.attachments = attachments;
73 $scope.crmMailingConst = CRM.crmMailing;
74
75 var ts = $scope.ts = CRM.ts(null);
76 var block = $scope.block = crmBlocker();
77
78 $scope.isSubmitted = function isSubmitted() {
79 return _.size($scope.mailing.jobs) > 0;
80 };
81
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();
92 });
93 return block(crmStatus({start: ts('Saving...'), success: ''}, savePromise)
94 .then(function () {
95 crmMailingPreviewMgr.sendTest(mailing, recipient);
96 }));
97 };
98
99 // @return Promise
100 $scope.submit = function submit() {
101 if (block.check() || $scope.crmMailing.$invalid) {
102 return;
103 }
104
105 var promise = crmMailingMgr.save($scope.mailing)
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 () {
114 $scope.leave('scheduled');
115 })
116 ;
117 return block(crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, promise));
118 };
119
120 // @return Promise
121 $scope.save = function save() {
122 return block(crmStatus(null,
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 })
129 ));
130 };
131
132 // @return Promise
133 $scope.delete = function cancel() {
134 return block(crmStatus({start: ts('Deleting...'), success: ts('Deleted')},
135 crmMailingMgr.delete($scope.mailing)
136 .then(function () {
137 $scope.leave('unscheduled');
138 })
139 ));
140 };
141
142 // @param string listingScreen 'archive', 'scheduled', 'unscheduled'
143 $scope.leave = function leave(listingScreen) {
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':
157 /* falls through */
158 default:
159 window.location = CRM.url('civicrm/mailing/browse/unscheduled', {
160 reset: 1,
161 scheduled: 'false'
162 });
163 }
164 };
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);
180 });
181
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
187 angular.module('crmMailing').controller('EditRecipCtrl', function EditRecipCtrl($scope, dialogService, crmApi, crmMailingMgr, $q, crmMetadata) {
188 var ts = $scope.ts = CRM.ts(null);
189 $scope.recipients = null;
190 $scope.getRecipientsEstimate = function () {
191 var ts = $scope.ts;
192 if ($scope.recipients === null) {
193 return ts('(Estimating)');
194 }
195 if ($scope.recipients.length === 0) {
196 return ts('No recipients');
197 }
198 if ($scope.recipients.length === 1) {
199 return ts('~1 recipient');
200 }
201 if (RECIPIENTS_PREVIEW_LIMIT > 0 && $scope.recipients.length >= RECIPIENTS_PREVIEW_LIMIT) {
202 return ts('>%1 recipients', {1: RECIPIENTS_PREVIEW_LIMIT});
203 }
204 return ts('~%1 recipients', {1: $scope.recipients.length});
205 };
206 $scope.getIncludesAsString = function () {
207 var first = true;
208 var names = '';
209 _.each($scope.mailing.recipients.groups.include, function (id) {
210 if (!first) {
211 names = names + ', ';
212 }
213 var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
214 names = names + group[0].title;
215 first = false;
216 });
217 _.each($scope.mailing.recipients.mailings.include, function (id) {
218 if (!first) {
219 names = names + ', ';
220 }
221 var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
222 names = names + oldMailing[0].name;
223 first = false;
224 });
225 return names;
226 };
227 $scope.getExcludesAsString = function () {
228 var first = true;
229 var names = '';
230 _.each($scope.mailing.recipients.groups.exclude, function (id) {
231 if (!first) {
232 names = names + ', ';
233 }
234 var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
235 names = names + group[0].title;
236 first = false;
237 });
238 _.each($scope.mailing.recipients.mailings.exclude, function (id) {
239 if (!first) {
240 names = names + ', ';
241 }
242 var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
243 names = names + oldMailing[0].name;
244 first = false;
245 });
246 return names;
247 };
248
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;
254 crmMailingMgr.previewRecipients($scope.mailing, RECIPIENTS_PREVIEW_LIMIT).then(function (recipients) {
255 $scope.recipients = recipients;
256 });
257 });
258 }, RECIPIENTS_DEBOUNCE_MS);
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);
263
264 $scope.previewRecipients = function previewRecipients() {
265 var model = {
266 recipients: $scope.recipients
267 };
268 var options = CRM.utils.adjustDialogDefaults({
269 autoOpen: false,
270 title: ts('Preview (%1)', {
271 1: $scope.getRecipientsEstimate()
272 })
273 });
274 dialogService.open('recipDialog', '~/crmMailing/dialog/recipients.html', model, options);
275 };
276
277 // Open a dialog for editing the advanced recipient options.
278 $scope.editOptions = function editOptions(mailing) {
279 var options = CRM.utils.adjustDialogDefaults({
280 autoOpen: false,
281 title: ts('Edit Options')
282 });
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 });
290 };
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
296 angular.module('crmMailing').controller('PreviewRecipCtrl', function ($scope) {
297 $scope.ts = CRM.ts(null);
298 });
299
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"
305 angular.module('crmMailing').controller('PreviewMailingDialogCtrl', function PreviewMailingDialogCtrl($scope) {
306 $scope.ts = CRM.ts(null);
307 });
308
309 // Controller for the "Recipients: Edit Options" dialog
310 // Note: Expects $scope.model to be an object with properties:
311 // - "mailing" (APIv3 mailing object)
312 // - "fields" (list of fields)
313 angular.module('crmMailing').controller('EditRecipOptionsDialogCtrl', function EditRecipOptionsDialogCtrl($scope) {
314 $scope.ts = CRM.ts(null);
315 });
316
317 // Controller for the "Preview Mailing Component" segment
318 // which displays header/footer/auto-responder
319 angular.module('crmMailing').controller('PreviewComponentCtrl', function PreviewComponentCtrl($scope, dialogService) {
320 var ts = $scope.ts = CRM.ts(null);
321
322 $scope.previewComponent = function previewComponent(title, componentId) {
323 var component = _.where(CRM.crmMailing.headerfooterList, {id: "" + componentId});
324 if (!component || !component[0]) {
325 CRM.alert(ts('Invalid component ID (%1)', {
326 1: componentId
327 }));
328 return;
329 }
330 var options = CRM.utils.adjustDialogDefaults({
331 autoOpen: false,
332 title: title // component[0].name
333 });
334 dialogService.open('previewComponentDialog', '~/crmMailing/dialog/previewComponent.html', component[0], options);
335 };
336 });
337
338 // Controller for the "Preview Mailing Component" dialog
339 // Note: Expects $scope.model to be an object with properties:
340 // - "name"
341 // - "subject"
342 // - "body_html"
343 // - "body_text"
344 angular.module('crmMailing').controller('PreviewComponentDialogCtrl', function PreviewComponentDialogCtrl($scope) {
345 $scope.ts = CRM.ts(null);
346 });
347
348 // Controller for the in-place msg-template management
349 angular.module('crmMailing').controller('MsgTemplateCtrl', function MsgTemplateCtrl($scope, crmMsgTemplates, dialogService) {
350 var ts = $scope.ts = CRM.ts(null);
351 $scope.crmMsgTemplates = crmMsgTemplates;
352
353 // @return Promise MessageTemplate (per APIv3)
354 $scope.saveTemplate = function saveTemplate(mailing) {
355 var model = {
356 selected_id: mailing.msg_template_id,
357 tpl: {
358 msg_title: '',
359 msg_subject: mailing.subject,
360 msg_text: mailing.body_text,
361 msg_html: mailing.body_html
362 }
363 };
364 var options = CRM.utils.adjustDialogDefaults({
365 autoOpen: false,
366 title: ts('Save Template')
367 });
368 return dialogService.open('saveTemplateDialog', '~/crmMailing/dialog/saveTemplate.html', model, options)
369 .then(function (item) {
370 mailing.msg_template_id = item.id;
371 return item;
372 });
373 };
374
375 // @param int id
376 // @return Promise
377 $scope.loadTemplate = function loadTemplate(mailing, id) {
378 return crmMsgTemplates.get(id).then(function (tpl) {
379 mailing.subject = tpl.msg_subject;
380 mailing.body_text = tpl.msg_text;
381 mailing.body_html = tpl.msg_html;
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
394 angular.module('crmMailing').controller('SaveMsgTemplateDialogCtrl', function SaveMsgTemplateDialogCtrl($scope, crmMsgTemplates, dialogService) {
395 var ts = $scope.ts = CRM.ts(null);
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).
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 ];
460 dialogService.setButtons('saveTemplateDialog', buttons);
461 }
462
463 setTimeout(scopeApply(init), 0);
464 });
465
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
478 $scope.crmFromAddresses = crmFromAddresses;
479 $scope.checkReplyToChange = function checkReplyToChange(mailing) {
480 if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
481 mailing.override_verp = '1';
482 changeAlert(ts('Reply-To'), ts('Track Replies'));
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 };
491 });
492
493 var lastEmailTokenAlert = null;
494 angular.module('crmMailing').controller('EmailBodyCtrl', function EmailBodyCtrl($scope, crmMailingMgr, crmUiAlert, $timeout) {
495 var ts = CRM.ts(null);
496
497 // ex: if (!hasAllTokens(myMailing, 'body_text)) alert('Oh noes!');
498 $scope.hasAllTokens = function hasAllTokens(mailing, field) {
499 return _.isEmpty(crmMailingMgr.findMissingTokens(mailing, field));
500 };
501
502 // ex: checkTokens(myMailing, 'body_text', 'insert:body_text')
503 // ex: checkTokens(myMailing, '*')
504 $scope.checkTokens = function checkTokens(mailing, field, insertEvent) {
505 if (lastEmailTokenAlert) {
506 lastEmailTokenAlert.close();
507 }
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 })
536 });
537 }
538 };
539 });
540 })(angular, CRM.$, CRM._);