Added views for bounced/unsubscribed/complained etc.

This commit is contained in:
Andris Reinman 2016-05-14 12:49:42 +03:00
parent 97803b74ed
commit 408f021c36
9 changed files with 437 additions and 29 deletions

View file

@ -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) => { module.exports.getByCid = (cid, callback) => {
cid = (cid || '').toString().trim(); cid = (cid || '').toString().trim();
if (!cid) { if (!cid) {

View file

@ -1,3 +1,3 @@
{ {
"schemaVersion": 10 "schemaVersion": 11
} }

View file

@ -321,8 +321,11 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
// show only messages that weren't bounced as delivered // show only messages that weren't bounced as delivered
campaign.delivered = campaign.delivered - campaign.bounced; campaign.delivered = campaign.delivered - campaign.bounced;
campaign.openRate = campaign.delivered ? Math.round((campaign.opened / 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) * 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) => { campaigns.getLinks(campaign.id, (err, links) => {
if (err) { 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) => { router.get('/clicked/:id/:linkId', passport.csrfProtection, (req, res) => {
campaigns.get(req.params.id, true, (err, campaign) => { campaigns.get(req.params.id, true, (err, campaign) => {
if (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 ? '<span class="datestring" data-date="' + row.updated.toISOString() + '" title="' + row.updated.toISOString() + '">' + row.updated.toISOString() + '</span>' : 'N/A',
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + campaign.list + '/edit/' + row.cid + '">Edit</a>'
])
});
});
});
});
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => { router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.delete(req.body.id, (err, deleted) => { campaigns.delete(req.body.id, (err, deleted) => {
if (err) { if (err) {

View file

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

View file

@ -0,0 +1,54 @@
<ol class="breadcrumb">
<li><a href="/">Home</a></li>
<li><a href="/campaigns">Campaigns</a></li>
{{#if parent}}
<li><a href="/campaigns/view/{{parent.id}}">{{parent.name}}</a></li>
{{/if}}
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
<li class="active">Bounced info</li>
</ol>
<h2><span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> {{name}} <small>Bounced info</small> <a class="btn btn-default btn-xs" href="/campaigns/view/{{id}}?tab=overview" role="button"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> View campaign</a></h2>
<hr>
{{#if description}}
<div class="well well-sm">{{{description}}}</div>
{{/if}}
<div class="table-responsive">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">Subscribers who bounced and were unsubscribed:</div>
<div class="panel-body">
<div class="table-responsive">
<table data-topic-url="/campaigns/status" data-topic-id="{{id}}/3" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,0,1,0">
<thead>
<tr>
<th class="col-md-1">
#
</th>
<th>
Address
</th>
<th>
First Name
</th>
<th>
Last Name
</th>
<th>
SMTP response
</th>
<th>
Bounced
</th>
<th></th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,54 @@
<ol class="breadcrumb">
<li><a href="/">Home</a></li>
<li><a href="/campaigns">Campaigns</a></li>
{{#if parent}}
<li><a href="/campaigns/view/{{parent.id}}">{{parent.name}}</a></li>
{{/if}}
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
<li class="active">Complained info</li>
</ol>
<h2><span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> {{name}} <small>Complained info</small> <a class="btn btn-default btn-xs" href="/campaigns/view/{{id}}?tab=overview" role="button"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> View campaign</a></h2>
<hr>
{{#if description}}
<div class="well well-sm">{{{description}}}</div>
{{/if}}
<div class="table-responsive">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">Subscribers who complained and were unsubscribed:</div>
<div class="panel-body">
<div class="table-responsive">
<table data-topic-url="/campaigns/status" data-topic-id="{{id}}/4" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,0,1,0">
<thead>
<tr>
<th class="col-md-1">
#
</th>
<th>
Address
</th>
<th>
First Name
</th>
<th>
Last Name
</th>
<th>
SMTP response
</th>
<th>
Complained
</th>
<th></th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,54 @@
<ol class="breadcrumb">
<li><a href="/">Home</a></li>
<li><a href="/campaigns">Campaigns</a></li>
{{#if parent}}
<li><a href="/campaigns/view/{{parent.id}}">{{parent.name}}</a></li>
{{/if}}
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
<li class="active">Delivered info</li>
</ol>
<h2><span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> {{name}} <small>Delivered info</small> <a class="btn btn-default btn-xs" href="/campaigns/view/{{id}}?tab=overview" role="button"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> View campaign</a></h2>
<hr>
{{#if description}}
<div class="well well-sm">{{{description}}}</div>
{{/if}}
<div class="table-responsive">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">Subscribers who received the message:</div>
<div class="panel-body">
<div class="table-responsive">
<table data-topic-url="/campaigns/status" data-topic-id="{{id}}/1" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,0,1,0">
<thead>
<tr>
<th class="col-md-1">
#
</th>
<th>
Address
</th>
<th>
First Name
</th>
<th>
Last Name
</th>
<th>
SMTP response
</th>
<th>
Delivered
</th>
<th></th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,54 @@
<ol class="breadcrumb">
<li><a href="/">Home</a></li>
<li><a href="/campaigns">Campaigns</a></li>
{{#if parent}}
<li><a href="/campaigns/view/{{parent.id}}">{{parent.name}}</a></li>
{{/if}}
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
<li class="active">Unsubscribed info</li>
</ol>
<h2><span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> {{name}} <small>Unsubscribed info</small> <a class="btn btn-default btn-xs" href="/campaigns/view/{{id}}?tab=overview" role="button"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> View campaign</a></h2>
<hr>
{{#if description}}
<div class="well well-sm">{{{description}}}</div>
{{/if}}
<div class="table-responsive">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">Subscribers who unsubscribed:</div>
<div class="panel-body">
<div class="table-responsive">
<table data-topic-url="/campaigns/status" data-topic-id="{{id}}/2" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,0,1,0">
<thead>
<tr>
<th class="col-md-1">
#
</th>
<th>
Address
</th>
<th>
First Name
</th>
<th>
Last Name
</th>
<th>
SMTP response
</th>
<th>
Unsubscribed
</th>
<th></th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>

View file

@ -88,37 +88,59 @@
{{#if isNormal}} {{#if isNormal}}
{{#unless isIdling}} {{#unless isIdling}}
<dt>Delivered</dt> <dt>Delivered <a href="/campaigns/status/{{id}}/delivered" title="List subscribers who received this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
<dd>{{delivered}}</dd> <dd>{{delivered}}</dd>
<dt>Bounced</dt> </dl>
<dd>{{bounced}}</dd> <hr />
<dl class="dl-horizontal">
<dt>Complaints</dt> <dt>Bounced <a href="/campaigns/status/{{id}}/bounced" title="List subscribers who bounced"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
<dd>{{complained}}</dd> <dd>
<div class="progress">
<div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="{{bounceRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{bounceRate}}%;">
{{bounced}}&nbsp;({{bounceRate}}%)
</div>
</div>
</dd>
<dt>Unsubscribed</dt> <dt>Complaints <a href="/campaigns/status/{{id}}/complained" title="List subscribers who complained for this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
<dd>{{unsubscribed}}</dd> <dd>
<div class="progress">
<div class="progress-bar progress-bar-danger" role="progressbar" aria-valuenow="{{complaintRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{complaintRate}}%;">
{{complained}}&nbsp;({{complaintRate}}%)
</div>
</div>
</dd>
<dt><a href="/campaigns/opened/{{id}}" title="List subscribers who opened this message">Opened <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></a></dt> <dt>Unsubscribed <a href="/campaigns/status/{{id}}/unsubscribed" title="List subscribers who unsubscribed after this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
<dd> <dd>
<div class="progress"> <div class="progress">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{openRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 2em; width: {{openRate}}%;"> <div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="{{unsubscribeRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{unsubscribeRate}}%;">
{{openRate}}% {{unsubscribed}}&nbsp;({{unsubscribeRate}}%)
</div> </div>
</div> </div>
</dd> </dd>
<dt><a href="/campaigns/clicked/{{id}}/all" title="List subscribers who clicked on a link">Clicked <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></a></dt> <dt>Opened <a href="/campaigns/opened/{{id}}" title="List subscribers who opened this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
<dd> <dd>
<div class="progress"> <div class="progress">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{clicksRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 2em; width: {{clicksRate}}%;"> <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{openRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{openRate}}%;">
{{clicksRate}}% {{opened}}&nbsp;({{openRate}}%)
</div> </div>
</div> </div>
</dd> </dd>
{{/unless}} <dt>Clicked <a href="/campaigns/clicked/{{id}}/all" title="List subscribers who clicked on a link"> <span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
<dd>
<div class="progress">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{clicksRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{clicksRate}}%;">
{{clicks}}&nbsp;({{clicksRate}}%)
</div>
</div>
</dd>
{{/unless}}
{{/if}} {{/if}}
</dl> </dl>
@ -295,7 +317,7 @@
</td> </td>
<td> <td>
<div class="pull-right"> <div class="pull-right">
<a href="/campaigns/clicked/{{../id}}/{{id}}" title="List subscribers who clicked this link"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></a> <a href="/campaigns/clicked/{{../id}}/{{id}}" title="List subscribers who clicked this link"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a>
</div> </div>
{{clicks}} {{clicks}}
</td> </td>
@ -323,7 +345,7 @@
</th> </th>
<th> <th>
<div class="pull-right"> <div class="pull-right">
<a href="/campaigns/clicked/{{id}}/all" title="List subscribers who clicked on a link"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></a> <a href="/campaigns/clicked/{{id}}/all" title="List subscribers who clicked on a link"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a>
</div> </div>
{{clicks}} {{clicks}}
</th> </th>