diff --git a/lib/models/campaigns.js b/lib/models/campaigns.js
index 4386080f..34816e73 100644
--- a/lib/models/campaigns.js
+++ b/lib/models/campaigns.js
@@ -194,6 +194,74 @@ module.exports.filterClickedSubscribers = (campaign, linkId, request, columns, c
};
+module.exports.filterStatusSubscribers = (campaign, status, request, columns, callback) => {
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ status = Number(status) || 0;
+ let query = 'SELECT COUNT(`subscription__' + campaign.list + '`.`id`) AS total FROM `subscription__' + campaign.list + '` JOIN `campaign__' + campaign.id + '` ON `campaign__' + campaign.id + '`.`list`=? AND `campaign__' + campaign.id + '`.`segment`=? AND `campaign__' + campaign.id + '`.`subscription`=`subscription__' + campaign.list + '`.`id` WHERE `campaign__' + campaign.id + '`.`status`=?';
+ let values = [campaign.list, campaign.segment, status];
+
+ connection.query(query, values, (err, total) => {
+ if (err) {
+ connection.release();
+ return callback(err);
+ }
+ total = total && total[0] && total[0].total || 0;
+
+ let ordering = [];
+
+ if (request.order && request.order.length) {
+
+ request.order.forEach(order => {
+ let orderField = columns[Number(order.column)];
+ let orderDirection = (order.dir || '').toString().toLowerCase() === 'desc' ? 'DESC' : 'ASC';
+ if (orderField) {
+ ordering.push('`' + orderField + '` ' + orderDirection);
+ }
+ });
+ }
+
+ if (!ordering.length) {
+ ordering.push('`email` ASC');
+ }
+
+ let args = [Number(request.length) || 50, Number(request.start) || 0];
+
+ if (request.search && request.search.value) {
+ query = 'SELECT SQL_CALC_FOUND_ROWS * FROM `subscription__' + campaign.list + '` JOIN `campaign__' + campaign.id + '` ON `campaign__' + campaign.id + '`.`list`=? AND `campaign__' + campaign.id + '`.`segment`=? AND `campaign__' + campaign.id + '`.`subscription`=`subscription__' + campaign.list + '`.`id` WHERE `campaign__' + campaign.id + '`.`status`=? AND (email LIKE ? OR first_name LIKE ? OR last_name LIKE ?) ORDER BY ' + ordering.join(', ') + ' LIMIT ? OFFSET ?';
+
+ let searchVal = '%' + request.search.value.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1') + '%';
+ args = values.concat([searchVal, searchVal, searchVal]).concat(args);
+ } else {
+ query = 'SELECT SQL_CALC_FOUND_ROWS * FROM `subscription__' + campaign.list + '` JOIN `campaign__' + campaign.id + '` ON `campaign__' + campaign.id + '`.`list`=? AND `campaign__' + campaign.id + '`.`segment`=? AND `campaign__' + campaign.id + '`.`subscription`=`subscription__' + campaign.list + '`.`id` WHERE `campaign__' + campaign.id + '`.`status`=? ORDER BY ' + ordering.join(', ') + ' LIMIT ? OFFSET ?';
+ args = values.concat(args);
+ }
+
+ connection.query(query, args, (err, rows) => {
+ if (err) {
+ connection.release();
+ return callback(err);
+ }
+ connection.query('SELECT FOUND_ROWS() AS total', (err, filteredTotal) => {
+ connection.release();
+ if (err) {
+ return callback(err);
+ }
+
+ let subscriptions = rows.map(row => tools.convertKeys(row));
+
+ filteredTotal = filteredTotal && filteredTotal[0] && filteredTotal[0].total || 0;
+ return callback(null, subscriptions, total, filteredTotal);
+ });
+ });
+ });
+ });
+
+};
+
module.exports.getByCid = (cid, callback) => {
cid = (cid || '').toString().trim();
if (!cid) {
diff --git a/meta.json b/meta.json
index bab3b5df..365ba1cb 100644
--- a/meta.json
+++ b/meta.json
@@ -1,3 +1,3 @@
{
- "schemaVersion": 10
+ "schemaVersion": 11
}
diff --git a/routes/campaigns.js b/routes/campaigns.js
index 48c37967..2ede36d9 100644
--- a/routes/campaigns.js
+++ b/routes/campaigns.js
@@ -321,8 +321,11 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
// show only messages that weren't bounced as delivered
campaign.delivered = campaign.delivered - campaign.bounced;
- campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 100) : 0;
- campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
+ campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 10000)/100 : 0;
+ campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 10000)/100 : 0;
+ campaign.bounceRate = campaign.delivered ? Math.round((campaign.bounced / campaign.delivered) * 10000)/100 : 0;
+ campaign.complaintRate = campaign.delivered ? Math.round((campaign.complained / campaign.delivered) * 10000)/100 : 0;
+ campaign.unsubscribeRate = campaign.delivered ? Math.round((campaign.unsubscribed / campaign.delivered) * 10000)/100 : 0;
campaigns.getLinks(campaign.id, (err, links) => {
if (err) {
@@ -373,6 +376,52 @@ router.get('/opened/:id', passport.csrfProtection, (req, res) => {
});
});
+router.get('/status/:id/:status', passport.csrfProtection, (req, res) => {
+ let id = Number(req.params.id) || 0;
+ let status;
+ switch (req.params.status) {
+ case 'delivered':
+ status = 1;
+ break;
+ case 'unsubscribed':
+ status = 2;
+ break;
+ case 'bounced':
+ status = 3;
+ break;
+ case 'complained':
+ status = 4;
+ break;
+ default:
+ req.flash('danger', 'Unknown status selector');
+ return res.redirect('/campaigns');
+ }
+
+ campaigns.get(id, true, (err, campaign) => {
+ if (err || !campaign) {
+ req.flash('danger', err && err.message || err || 'Could not find campaign with specified ID');
+ return res.redirect('/campaigns');
+ }
+
+ lists.get(campaign.list, (err, list) => {
+ if (err || !campaign) {
+ req.flash('danger', err && err.message || err);
+ return res.redirect('/campaigns');
+ }
+
+ campaign.csrfToken = req.csrfToken();
+ campaign.list = list;
+
+ // show only messages that weren't bounced as delivered
+ campaign.delivered = campaign.delivered - campaign.bounced;
+ campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
+ campaign.status = status;
+
+ res.render('campaigns/' + req.params.status, campaign);
+ });
+ });
+});
+
router.get('/clicked/:id/:linkId', passport.csrfProtection, (req, res) => {
campaigns.get(req.params.id, true, (err, campaign) => {
if (err || !campaign) {
@@ -460,6 +509,44 @@ router.post('/clicked/ajax/:id/:linkId', (req, res) => {
});
});
+router.post('/status/ajax/:id/:status', (req, res) => {
+ let status = Number(req.params.status) || 0;
+
+ campaigns.get(req.params.id, true, (err, campaign) => {
+ if (err || !campaign) {
+ return res.json({
+ error: err && err.message || err || 'Campaign not found',
+ data: []
+ });
+ }
+
+ let columns = ['#', 'email', 'first_name', 'last_name', 'campaign__' + campaign.id + '`.`updated'];
+ campaigns.filterStatusSubscribers(campaign, status, req.body, columns, (err, data, total, filteredTotal) => {
+ if (err) {
+ return res.json({
+ error: err.message || err,
+ data: []
+ });
+ }
+
+ res.json({
+ draw: req.body.draw,
+ recordsTotal: total,
+ recordsFiltered: filteredTotal,
+ data: data.map((row, i) => [
+ (Number(req.body.start) || 0) + 1 + i,
+ htmlescape(row.email || ''),
+ htmlescape(row.firstName || ''),
+ htmlescape(row.lastName || ''),
+ htmlescape(row.response || ''),
+ row.updated && row.created.toISOString ? '' + row.updated.toISOString() + '' : 'N/A',
+ 'Edit'
+ ])
+ });
+ });
+ });
+});
+
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.delete(req.body.id, (err, deleted) => {
if (err) {
diff --git a/setup/sql/upgrade-00011.sql b/setup/sql/upgrade-00011.sql
new file mode 100644
index 00000000..59f14fef
--- /dev/null
+++ b/setup/sql/upgrade-00011.sql
@@ -0,0 +1,15 @@
+# Header section
+# Define incrementing schema version number
+SET @schema_version = '11';
+
+-- {{#each tables.campaign}}
+
+ # Adds new index for 'status' on campaign messages table
+ CREATE INDEX status_index ON `{{this}}` (`status`);
+
+-- {{/each}}
+
+# Footer section
+LOCK TABLES `settings` WRITE;
+INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
+UNLOCK TABLES;
diff --git a/views/campaigns/bounced.hbs b/views/campaigns/bounced.hbs
new file mode 100644
index 00000000..c35cc3a0
--- /dev/null
+++ b/views/campaigns/bounced.hbs
@@ -0,0 +1,54 @@
+
+ - Home
+ - Campaigns
+ {{#if parent}}
+ - {{parent.name}}
+ {{/if}}
+ - {{name}}
+ - Bounced info
+
+
+
+
+
+
+{{#if description}}
+ {{{description}}}
+{{/if}}
+
+
+
+
+
+
Subscribers who bounced and were unsubscribed:
+
+
+
+
+
+
+ #
+ |
+
+ Address
+ |
+
+ First Name
+ |
+
+ Last Name
+ |
+
+ SMTP response
+ |
+
+ Bounced
+ |
+ |
+
+
+
+
+
+
+
diff --git a/views/campaigns/complained.hbs b/views/campaigns/complained.hbs
new file mode 100644
index 00000000..77f1b41a
--- /dev/null
+++ b/views/campaigns/complained.hbs
@@ -0,0 +1,54 @@
+
+ - Home
+ - Campaigns
+ {{#if parent}}
+ - {{parent.name}}
+ {{/if}}
+ - {{name}}
+ - Complained info
+
+
+
+
+
+
+{{#if description}}
+ {{{description}}}
+{{/if}}
+
+
+
+
+
+
Subscribers who complained and were unsubscribed:
+
+
+
+
+
+
+ #
+ |
+
+ Address
+ |
+
+ First Name
+ |
+
+ Last Name
+ |
+
+ SMTP response
+ |
+
+ Complained
+ |
+ |
+
+
+
+
+
+
+
diff --git a/views/campaigns/delivered.hbs b/views/campaigns/delivered.hbs
new file mode 100644
index 00000000..fec89f6d
--- /dev/null
+++ b/views/campaigns/delivered.hbs
@@ -0,0 +1,54 @@
+
+ - Home
+ - Campaigns
+ {{#if parent}}
+ - {{parent.name}}
+ {{/if}}
+ - {{name}}
+ - Delivered info
+
+
+
+
+
+
+{{#if description}}
+ {{{description}}}
+{{/if}}
+
+
+
+
+
+
Subscribers who received the message:
+
+
+
+
+
+
+ #
+ |
+
+ Address
+ |
+
+ First Name
+ |
+
+ Last Name
+ |
+
+ SMTP response
+ |
+
+ Delivered
+ |
+ |
+
+
+
+
+
+
+
diff --git a/views/campaigns/unsubscribed.hbs b/views/campaigns/unsubscribed.hbs
new file mode 100644
index 00000000..c6440dfa
--- /dev/null
+++ b/views/campaigns/unsubscribed.hbs
@@ -0,0 +1,54 @@
+
+ - Home
+ - Campaigns
+ {{#if parent}}
+ - {{parent.name}}
+ {{/if}}
+ - {{name}}
+ - Unsubscribed info
+
+
+ {{name}} Unsubscribed info View campaign
+
+
+
+{{#if description}}
+ {{{description}}}
+{{/if}}
+
+
+
+
+
+
Subscribers who unsubscribed:
+
+
+
+
+
+
+ #
+ |
+
+ Address
+ |
+
+ First Name
+ |
+
+ Last Name
+ |
+
+ SMTP response
+ |
+
+ Unsubscribed
+ |
+ |
+
+
+
+
+
+
+
diff --git a/views/campaigns/view.hbs b/views/campaigns/view.hbs
index 33b3f662..cbe84648 100644
--- a/views/campaigns/view.hbs
+++ b/views/campaigns/view.hbs
@@ -88,37 +88,59 @@
{{#if isNormal}}
{{#unless isIdling}}
- Delivered
+ Delivered
{{delivered}}
- Bounced
- {{bounced}}
+
+
+
- - Complaints
- - {{complained}}
+ - Bounced
+ -
+
+
+ {{bounced}} ({{bounceRate}}%)
+
+
+
- - Unsubscribed
- - {{unsubscribed}}
+ - Complaints
+ -
+
+
+ {{complained}} ({{complaintRate}}%)
+
+
+
- - Opened
- -
-
-
+ - Unsubscribed
+ -
+
+
+ {{unsubscribed}} ({{unsubscribeRate}}%)
+
+
+
- - Clicked
- -
-
-
+ - Opened
+ -
+
+
+ {{opened}} ({{openRate}}%)
+
+
+
- {{/unless}}
+ - Clicked
+ -
+
+
+ {{clicks}} ({{clicksRate}}%)
+
+
+
+
+ {{/unless}}
{{/if}}
@@ -295,7 +317,7 @@
{{clicks}}
|
@@ -323,7 +345,7 @@
{{clicks}}
|