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 @@ + + +

{{name}} Bounced info View campaign

+ +
+ +{{#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 @@ + + +

{{name}} Complained info View campaign

+ +
+ +{{#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 @@ + + +

{{name}} Delivered info View campaign

+ +
+ +{{#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 @@ + + +

{{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
-
-
-
- {{openRate}}% -
-
-
+
Unsubscribed
+
+
+
+ {{unsubscribed}} ({{unsubscribeRate}}%) +
+
+
-
Clicked
-
-
-
- {{clicksRate}}% -
-
-
+
Opened
+
+
+
+ {{opened}} ({{openRate}}%) +
+
+
- {{/unless}} +
Clicked
+
+
+
+ {{clicks}} ({{clicksRate}}%) +
+
+
+ + {{/unless}} {{/if}}
@@ -295,7 +317,7 @@
- +
{{clicks}} @@ -323,7 +345,7 @@
- +
{{clicks}}