Skip to content

Commit

Permalink
feat(reports): implement report emailing
Browse files Browse the repository at this point in the history
This commit implements the email dropdown on reports as a first step
towards automatic email reporting.  The automatic email reports will
depend on a scheduler proposed in Third-Culture-Software#2308.

The email functionality depends on having a mailgun account. You should
put your mailgun API key and domain in the .env.development file.  Some
defaults for this project have been filled out.

Partially addresses Third-Culture-Software#1688 and Third-Culture-Software#1766.
  • Loading branch information
jniles committed Nov 28, 2017
1 parent 28cf031 commit c5a66e1
Show file tree
Hide file tree
Showing 16 changed files with 539 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ DEBUG=app

UPLOAD_DIR='client/upload'

# Mailgun Creds
MAILGUN_API_KEY="key-xyz"
MAILGUN_DOMAIN="bhi.ma"
MAILGUN_SERVER_ADDRESS="[email protected]"

# control Redis Pub/Sub
ENABLE_EVENTS=false
1 change: 1 addition & 0 deletions client/src/i18n/en/form.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"DISABLED_CURRENCY": "This currency is unusable since there is no account set for it. Use the Cashbox Management module to set an account for this currency and cashbox.",
"EDITED" : "Edited",
"ENTITY_NOT_FOUND": "Entity (Debtor/Creditor) Not Found",
"EMAIL_SUCCESS" : "Email successfully delivered.",
"EXPORT_SUCCESS": "Successfully exported",
"FINANCIAL_DETAIL": "Financial details",
"FOUND": "Found",
Expand Down
3 changes: 2 additions & 1 deletion client/src/i18n/en/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"BACK": "Back to the previous page",
"OPTIONS": "Options",
"PREVIEW": "Preview",
"VIEW_ARCHIVE": "Archives"
"VIEW_ARCHIVE": "Archives",
"EMAIL_HELP_TXT" : "The email above will receive the report as an attachment. The server must have an internet connection for the email to be successfully delivered."
}
}
}
1 change: 1 addition & 0 deletions client/src/i18n/fr/form.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"DELETE_SUCCESS": "Suppression avec succès",
"DISABLED_CURRENCY": "Cette monnaie est inutilisable car il n'y a pas de compte fixée pour elle. Utilisez le module Box Cash Management pour définir un compte pour cette monnaie et caisse.",
"ENTITY_NOT_FOUND": "Entité (Debiteur/Crediteur) Non Trouvé",
"EMAIL_SUCCESS" : "Email envoyé avec succès.",
"EXPORT_SUCCESS": "Exportation avec succes",
"EDITED" : "Édité",
"FINANCIAL_DETAIL": "Détails financieres",
Expand Down
3 changes: 2 additions & 1 deletion client/src/i18n/fr/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@
"BACK":"Revenir en arriere",
"OPTIONS":"Options",
"PREVIEW":"Aperçu",
"VIEW_ARCHIVE":"Archives"
"VIEW_ARCHIVE":"Archives",
"EMAIL_HELP_TXT" : "L'e-mail ci-dessus recevra le rapport en pièce jointe. Le serveur doit avoire une connexion Internet pour que l'e-mail soit livré avec succès."
}
}
}
21 changes: 21 additions & 0 deletions client/src/modules/reports/baseReports.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ function BaseReportService($http, Modal, util, Languages) {
service.saveReport = saveReport;
service.requestPreview = requestPreview;
service.saveAsModal = saveAsModal;
service.emailReportModal = emailReportModal;
service.emailReport = emailReport;

function requestKey(key) {
var url = '/reports/keys/';
Expand Down Expand Up @@ -63,6 +65,25 @@ function BaseReportService($http, Modal, util, Languages) {
.then(util.unwrapHttpResponse);
}

function emailReport(uuid, email) {
var url = '/reports/archive/'.concat(uuid, '/email');
return $http.put(url, { address : email })
.then(util.unwrapHttpResponse);
}

function emailReportModal(options) {
var instance = Modal.open({
keyboard : true,
resolve : {
options : function resolveOptions() { return options; },
},
controller : 'EmailReportController as EmailCtrl',
templateUrl : '/modules/reports/modals/reports.email.html',
});

return instance.result;
}

function saveAsModal(options) {
var instance = Modal.open({
animation : false,
Expand Down
36 changes: 36 additions & 0 deletions client/src/modules/reports/modals/reports.email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<form name="EmailForm" bh-submit="EmailCtrl.submit(EmailForm)" novalidate>
<div class="modal-header">
<ol class="headercrumb">
<li class="static">
<span translate>TREE.REPORTS</span>
</li>
<li class="title">{{ EmailCtrl.reportName }}</li>
</ol>
</div>

<div class="modal-body" style="max-height:600px;">
<div class="form-group" ng-class="{ 'has-error' : EmailForm.$submitted && EmailForm.email.$invalid }">
<label class="control-label" translate>FORM.LABELS.EMAIL</label>
<input name="email" type="email" ng-model="EmailCtrl.params.email" class="form-control" required>

<div class="help-block" ng-messages="EmailForm.email.$error" ng-show="EmailForm.$submitted">
<div ng-messages-include="modules/templates/messages.tmpl.html"></div>
</div>
</div>

<p class="text-primary">
<i class="fa fa-info-circle"></i>
<span translate>REPORT.UTIL.EMAIL_HELP_TXT</span>
</p>
</div>

<div class="modal-footer">
<button type="button" data-method="cancel" class="btn btn-default" ng-click="EmailCtrl.dismiss()">
<span translate>FORM.BUTTONS.CANCEL</span>
</button>

<bh-loading-button loading-state="EmailForm.$loading">
<span translate>FORM.BUTTONS.SUBMIT</span>
</bh-loading-button>
</div>
</form>
31 changes: 31 additions & 0 deletions client/src/modules/reports/modals/reports.email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
angular.module('bhima.controllers')
.controller('EmailReportController', EmailReportController);

EmailReportController.$inject = [
'$uibModalInstance', 'NotifyService', 'BaseReportService', 'options',
'SessionService',
];

function EmailReportController(ModalInstance, Notify, SavedReports, options, Session) {
var vm = this;

vm.reportName = options.reportName;

vm.params = {
email : Session.user.email,
};

vm.submit = function submit(EmailForm) {
if (EmailForm.$invalid) { return 1; }

return SavedReports.emailReport(options.uuid, vm.params.email)
.then(function () {
ModalInstance.close({ sent : true });
})
.catch(Notify.handleError);
};

vm.dismiss = function dismiss() {
ModalInstance.close({ sent : false });
};
}
22 changes: 18 additions & 4 deletions client/src/modules/reports/reportsArchive.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ ReportsArchiveController.$inject = [
function ReportsArchiveController($state, SavedReports, Notify, reportData) {
var vm = this;

var typeTemplate =
'<div class="ui-grid-cell-contents"><i class="fa fa-file-pdf-o"></i></div>';
var dateTemplate =
'<div class="ui-grid-cell-contents">{{ row.entity.timestamp | date }} (<span am-time-ago="row.entity.timestamp"></span>)</div>';
var printTemplate =
'<div class="ui-grid-cell-contents"><bh-pdf-link pdf-url="/reports/archive/{{row.entity.uuid}}"></bh-pdf-link></div>';

var reportId = reportData.id;
vm.key = $state.params.key;

vm.deleteReport = deleteReport;
vm.emailReport = emailReport;

vm.gridOptions = {
fastWatch : true,
Expand All @@ -21,10 +29,6 @@ function ReportsArchiveController($state, SavedReports, Notify, reportData) {
appScopeProvider : vm,
};

var typeTemplate = '<div class="ui-grid-cell-contents"><i class="fa fa-file-pdf-o"></i></div>';
var dateTemplate = '<div class="ui-grid-cell-contents">{{ row.entity.timestamp | date }} (<span am-time-ago="row.entity.timestamp"></span>)</div>';
var printTemplate = '<div class="ui-grid-cell-contents"><bh-pdf-link pdf-url="/reports/archive/{{row.entity.uuid}}"></bh-pdf-link></div>';

vm.gridOptions.columnDefs = [
{ field : 'typeicon', displayName : '', cellTemplate : typeTemplate, width : 25 },
{ field : 'label', displayName : 'FORM.LABELS.LABEL', headerCellFilter : 'translate' },
Expand All @@ -46,6 +50,16 @@ function ReportsArchiveController($state, SavedReports, Notify, reportData) {
.catch(Notify.handleError);
}

function emailReport(uid, name) {
SavedReports.emailReportModal({ uuid : uid, reportName : name })
.then(function (result) {
if (result.sent) {
Notify.success('FORM.INFO.EMAIL_SUCCESS');
}
})
.catch(Notify.handleError);
}

function loadSavedReports() {
vm.loading = true;

Expand Down
4 changes: 2 additions & 2 deletions client/src/modules/templates/actionsDropdown.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
</a>
</li>
<li>
<a href>
<span class="text-muted"><i class="fa fa-envelope"></i> <span translate>TABLE.COLUMNS.EMAIL</span></span>
<a href data-action="email" ng-click="grid.appScope.emailReport(row.entity.uuid, row.entity.label)">
<i class="fa fa-envelope"></i> <span translate>TABLE.COLUMNS.EMAIL</span>
</a>
</li>
<li>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@
"json-2-csv": "^2.1.0",
"json2xls": "^0.1.2",
"lodash": "^4.16.2",
"mailgun-js": "^0.13.1",
"mkdirp": "^0.5.1",
"moment": "^2.15.0",
"morgan": "^1.6.1",
"multer": "^1.1.0",
"mysql": "^2.14.0",
"mz": "^2.7.0",
"q": "~1.5.0",
"snyk": "^1.41.1",
"stream-to-promise": "^2.2.0",
Expand Down
1 change: 1 addition & 0 deletions server/config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ exports.configure = function configure(app) {

// lookup saved report document
app.get('/reports/archive/:uuid', report.sendArchived);
app.put('/reports/archive/:uuid/email', report.emailArchived);
app.delete('/reports/archive/:uuid', report.deleteArchived);

app.get('/dashboard/debtors', dashboardDebtors.getReport);
Expand Down
94 changes: 83 additions & 11 deletions server/controllers/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,27 @@
* @requires path
* @requires fs
* @requires db
* @requires debug
* @requires moment
* @requires lib/barcode
* @requires lib/errors/BadRequest
* @requires lib/mailer
*/

// const path = require('path');
const fs = require('fs');
const db = require('../lib/db');
const barcode = require('../lib/barcode');
const BadRequest = require('../lib/errors/BadRequest');
const moment = require('moment');
const mailer = require('../lib/mailer');
const debug = require('debug')('reports');

exports.keys = keys;
exports.list = list;
exports.sendArchived = sendArchived;
exports.deleteArchived = deleteArchived;
exports.emailArchived = emailArchived;

exports.barcodeLookup = barcodeLookup;
exports.barcodeRedirect = barcodeRedirect;
Expand All @@ -39,7 +48,7 @@ exports.barcodeRedirect = barcodeRedirect;
* GET /reports/keys/:key
*/
function keys(req, res, next) {
const key = req.params.key;
const { key } = req.params;
const sql = `SELECT * FROM report WHERE report_key = ?;`;

db.exec(sql, [key])
Expand Down Expand Up @@ -72,14 +81,13 @@ function keys(req, res, next) {
* GET /reports/saved/:reportId
*/
function list(req, res, next) {
const reportId = req.params.reportId;
const sql =
`
SELECT
BUID(saved_report.uuid) as uuid, label, report_id,
parameters, link, timestamp, user_id,
user.display_name
FROM saved_report left join user on saved_report.user_id = user.id
const { reportId } = req.params;
const sql = `
SELECT
BUID(saved_report.uuid) as uuid, label, report_id,
parameters, link, timestamp, user_id,
user.display_name
FROM saved_report left join user on saved_report.user_id = user.id
WHERE report_id = ?`;

db.exec(sql, [reportId])
Expand Down Expand Up @@ -158,10 +166,74 @@ function deleteArchived(req, res, next) {
.done();
}

// TODO(@jniles) - translate these emails into multiple languages
const REPORT_EMAIL =
`Hello!
Please find the attached report "%filename%" produced by %user% on %date%.
This email was requested by %requestor%.
Thanks,
bhi.ma
`;

// this is a really quick and lazy templating scheme
const template = (str, values) => {
return Object.keys(values).reduce((formatted, key) =>
formatted.replace(`%${key}%`, values[key]), str);
};


/**
* @function emailArchived
*
* @description
* Emails an archived report to an email address provided in the "to" field.
*/
function emailArchived(req, res, next) {
const { uuid } = req.params;
const { address } = req.body;

debug(`#emailArchived(): Received email request for ${address}.`);

lookupArchivedReport(uuid)
.then(report => {
debug(`#emailArchived(): sending ${report.label} to ${address}.`);

const date = moment(report.timestamp).format('YYYY-MM-DD');
const filename = `${report.label}.pdf`;

const attachments = [
{ filename, path : report.link },
];

// template parameters for the email
const parameters = {
filename,
date,
user : report.display_name,
requestor : req.session.user.display_name,
};

// template in the parameters into message body
const message = template(REPORT_EMAIL, parameters);
const subject = `${report.label} - ${date}`;

return mailer.email(address, subject, message, { attachments });
})
.then(() => {
debug(`#emailArchived(): email sent to ${address}.`);
res.sendStatus(200);
})
.catch(next)
.done();
}

// Method to return the object
// Method to redirect
function barcodeLookup(req, res, next) {
const key = req.params.key;
const { key } = req.params;

barcode.reverseLookup(key)
.then(result => res.send(result))
Expand All @@ -170,7 +242,7 @@ function barcodeLookup(req, res, next) {
}

function barcodeRedirect(req, res, next) {
const key = req.params.key;
const { key } = req.params;

barcode.reverseLookup(key)
// populated by barcode controller
Expand Down
Loading

0 comments on commit c5a66e1

Please sign in to comment.