Merge pull request #5220 from totten/master-recip-grptype
[civicrm-core.git] / js / angular-crmMailing / services.js
index 5dbc803a1a574e9461eb5c95f6ab03da5de8d1ad..1577f40d17618b6c4acf85b73de331c6ec729d15 100644 (file)
@@ -1,7 +1,4 @@
 (function (angular, $, _) {
-  var partialUrl = function (relPath) {
-    return '~/crmMailing/' + relPath;
-  };
 
   // The representation of from/reply-to addresses is inconsistent in the mailing data-model,
   // so the UI must do some adaptation. The crmFromAddresses provides a richer way to slice/dice
       _loadGroups: function (mailing) {
         return crmApi('MailingGroup', 'get', {mailing_id: mailing.id})
           .then(function (groupResult) {
-            mailing.groups = {include: [], exclude: []};
-            mailing.mailings = {include: [], exclude: []};
+            mailing.recipients = {};
+            mailing.recipients.groups = {include: [], exclude: [], base: []};
+            mailing.recipients.mailings = {include: [], exclude: []};
             _.each(groupResult.values, function (mailingGroup) {
-              var bucket = (mailingGroup.entity_table == 'civicrm_group') ? 'groups' : 'mailings';
+              var bucket = (/^civicrm_group/.test(mailingGroup.entity_table)) ? 'groups' : 'mailings';
               var entityId = parseInt(mailingGroup.entity_id);
-              mailing[bucket][mailingGroup.group_type].push(entityId);
+              mailing.recipients[bucket][mailingGroup.group_type.toLowerCase()].push(entityId);
             });
           });
       },
           });
       },
       // @return Object Mailing (per APIv3)
-      create: function create() {
-        return {
+      create: function create(params) {
+        var defaults = {
           jobs: {}, // {jobId: JobRecord}
+          recipients: {
+            groups: {include: [], exclude: [], base: []},
+            mailings: {include: [], exclude: []}
+          },
           name: "",
           campaign_id: null,
-          from_name: crmFromAddresses.getDefault().author,
-          from_email: crmFromAddresses.getDefault().email,
           replyto_email: "",
           subject: "",
-          groups: {include: [], exclude: []},
-          mailings: {include: [], exclude: []},
           body_html: "",
-          body_text: "",
-          footer_id: null, // pickDefaultMailComponent('Footer'),
-          header_id: null, // pickDefaultMailComponent('Header'),
-          visibility: "Public Pages",
-          url_tracking: "1",
-          dedupe_email: "1",
-          forward_replies: "0",
-          auto_responder: "0",
-          open_tracking: "1",
-          override_verp: "1",
-          optout_id: pickDefaultMailComponent('OptOut'),
-          reply_id: pickDefaultMailComponent('Reply'),
-          resubscribe_id: pickDefaultMailComponent('Resubscribe'),
-          unsubscribe_id: pickDefaultMailComponent('Unsubscribe')
+          body_text: ""
         };
+        return angular.extend({}, defaults, params);
       },
 
       // @param mailing Object (per APIv3)
         }
       },
 
+      // Search the body, header, and footer for required tokens.
+      // ex: var msgs = findMissingTokens(mailing, 'body_html');
+      findMissingTokens: function(mailing, field) {
+        var missing = {};
+        if (!_.isEmpty(mailing[field])) {
+          var body = '';
+          if (mailing.footer_id) {
+            var footer = _.where(CRM.crmMailing.headerfooterList, {id: mailing.footer_id});
+            body = body + footer[0][field];
+
+          }
+          body = body + mailing[field];
+          if (mailing.header_id) {
+            var header = _.where(CRM.crmMailing.headerfooterList, {id: mailing.header_id});
+            body = body + header[0][field];
+          }
+
+          angular.forEach(CRM.crmMailing.requiredTokens, function(value, token) {
+            if (!_.isObject(value)) {
+              if (body.indexOf('{' + token + '}') < 0) {
+                missing[token] = value;
+              }
+            }
+            else {
+              var count = 0;
+              angular.forEach(value, function(nestedValue, nestedToken) {
+                if (body.indexOf('{' + nestedToken + '}') >= 0) {
+                  count++;
+                }
+              });
+              if (count === 0) {
+                angular.extend(missing, value);
+              }
+            }
+          });
+        }
+        return missing;
+      },
+
       // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
       // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
       mergeInto: function mergeInto(mailingTgt, mailingFrom, excludes) {
           'replyto_email',
           'subject',
           'dedupe_email',
-          'groups',
-          'mailings',
+          'recipients',
           'body_html',
           'body_text',
           'footer_id',
       // @param mailing Object (per APIv3)
       // @return Promise an object with "subject", "body_text", "body_html"
       preview: function preview(mailing) {
-        var params = angular.extend({}, mailing, {
+        var params = angular.extend({}, mailing, mailing.recipients, {
           options: {force_rollback: 1},
           'api.Mailing.preview': {
             id: '$value.id'
           }
         });
+        delete params.recipients; // the content was merged in
         return crmApi('Mailing', 'create', params).then(function (result) {
           // changes rolled back, so we don't care about updating mailing
           return result.values[result.id]['api.Mailing.preview'].values;
       previewRecipients: function previewRecipients(mailing, previewLimit) {
         // To get list of recipients, we tentatively save the mailing and
         // get the resulting recipients -- then rollback any changes.
-        var params = angular.extend({}, mailing, {
+        var params = angular.extend({}, mailing, mailing.recipients, {
           name: 'placeholder', // for previewing recipients on new, incomplete mailing
           subject: 'placeholder', // for previewing recipients on new, incomplete mailing
           options: {force_rollback: 1},
             'api.email.getvalue': {'return': 'email'}
           }
         });
+        delete params.recipients; // the content was merged in
         return crmApi('Mailing', 'create', params).then(function (recipResult) {
           // changes rolled back, so we don't care about updating mailing
           return recipResult.values[recipResult.id]['api.MailingRecipients.get'].values;
       // Save a (draft) mailing
       // @param mailing Object (per APIv3)
       // @return Promise
-      save: function (mailing) {
-        var params = angular.extend({}, mailing, {
-          'api.mailing_job.create': 0 // note: exact match to API default
+      save: function(mailing) {
+        var params = angular.extend({}, mailing, mailing.recipients);
+
+        // Angular ngModel sometimes treats blank fields as undefined.
+        angular.forEach(mailing, function(value, key) {
+          if (value === undefined || value === null) {
+            mailing[key] = '';
+          }
         });
 
         // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
 
         delete params.jobs;
 
-        return crmApi('Mailing', 'create', params).then(function (result) {
+        delete params.recipients; // the content was merged in
+
+        return crmApi('Mailing', 'create', params).then(function(result) {
           if (result.id && !mailing.id) {
             mailing.id = result.id;
           }  // no rollback, so update mailing.id
       // @param to Object with either key "email" (string) or "gid" (int)
       // @return Promise for a list of delivery reports
       sendTest: function (mailing, recipient) {
-        var params = angular.extend({}, mailing, {
+        var params = angular.extend({}, mailing, mailing.recipients, {
           // options:  {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
           'api.Mailing.send_test': {
             mailing_id: '$value.id',
 
         delete params.jobs;
 
+        delete params.recipients; // the content was merged in
+
         return crmApi('Mailing', 'create', params).then(function (result) {
           if (result.id && !mailing.id) {
             mailing.id = result.id;
       // @return Promise
       preview: function preview(mailing, mode) {
         var templates = {
-          html: partialUrl('dialog/previewHtml.html'),
-          text: partialUrl('dialog/previewText.html'),
-          full: partialUrl('dialog/previewFull.html')
+          html: '~/crmMailing/dialog/previewHtml.html',
+          text: '~/crmMailing/dialog/previewText.html',
+          full: '~/crmMailing/dialog/previewFull.html'
         };
         var result = null;
         var p = crmMailingMgr
           .preview(mailing)
           .then(function (content) {
-            var options = {
+            var options = CRM.utils.adjustDialogDefaults({
               autoOpen: false,
-              modal: true,
               title: ts('Subject: %1', {
                 1: content.subject
               })
-            };
+            });
             result = dialogService.open('previewDialog', templates[mode], content, options);
           });
         crmStatus({start: ts('Previewing'), success: ''}, p);
     };
   });
 
+  angular.module('crmMailing').factory('crmMailingStats', function (crmApi, crmLegacy) {
+    var statTypes = [
+      // {name: 'Recipients', title: ts('Intended Recipients'),   searchFilter: '',                           eventsFilter: '&event=queue'},
+      {name: 'Delivered',     title: ts('Successful Deliveries'), searchFilter: '&mailing_delivery_status=Y', eventsFilter: '&event=delivered'},
+      {name: 'Opened',        title: ts('Tracked Opens'),         searchFilter: '&mailing_open_status=Y',     eventsFilter: '&event=opened'},
+      {name: 'Unique Clicks', title: ts('Click-throughs'),        searchFilter: '&mailing_click_status=Y',    eventsFilter: '&event=click&distinct=1'},
+      // {name: 'Forward',    title: ts('Forwards'),              searchFilter: '&mailing_forward=1',         eventsFilter: '&event=forward'},
+      // {name: 'Replies',    title: ts('Replies'),               searchFilter: '&mailing_reply_status=Y',    eventsFilter: '&event=reply'},
+      {name: 'Bounces',       title: ts('Bounces'),               searchFilter: '&mailing_delivery_status=N', eventsFilter: '&event=bounce'},
+      {name: 'Unsubscribers', title: ts('Unsubscribes'),          searchFilter: '&mailing_unsubscribe=1',     eventsFilter: '&event=unsubscribe'}
+      // {name: 'OptOuts',    title: ts('Opt-Outs'),              searchFilter: '&mailing_optout=1',          eventsFilter: '&event=optout'}
+    ];
+
+    return {
+      getStatTypes: function() {
+        return statTypes;
+      },
+
+      /**
+       * @param mailingIds object
+       *   List of mailing IDs ({a: 123, b: 456})
+       * @return Promise
+       *   List of stats for each mailing
+       *   ({a: ...object..., b: ...object...})
+       */
+      getStats: function(mailingIds) {
+        var params = {};
+        angular.forEach(mailingIds, function(mailingId, name) {
+          params[name] = ['Mailing', 'stats', {mailing_id: mailingId}];
+        });
+        return crmApi(params).then(function(result) {
+          var stats = {};
+          angular.forEach(mailingIds, function(mailingId, name) {
+            stats[name] = result[name].values[mailingId];
+          });
+          return stats;
+        });
+      },
+
+      /**
+       * Determine the legacy URL for a report about a given mailing and stat.
+       *
+       * @param mailing object
+       * @param statType object (see statTypes above)
+       * @param view string ('search', 'event', 'report')
+       * @return string|null
+       */
+      getUrl: function getUrl(mailing, statType, view) {
+        switch (view) {
+          case 'events':
+            return crmLegacy.url('civicrm/mailing/report/event',
+              'reset=1&mid=' + mailing.id + statType.eventsFilter);
+
+          case 'search':
+            return crmLegacy.url('civicrm/contact/search/advanced',
+              'force=1&mailing_id=' + mailing.id + statType.searchFilter);
+
+          // TODO: case 'report':
+          default:
+            return null;
+        }
+      }
+    };
+  });
+
 })(angular, CRM.$, CRM._);