mirror of
https://github.com/atlanticbiomedical/biomedjs.git
synced 2025-07-02 00:47:26 -04:00
Changes
This commit is contained in:
@ -1,42 +0,0 @@
|
||||
var mongoose = require('mongoose'),
|
||||
Clock = mongoose.model('Clock');
|
||||
|
||||
module.exports = function(piler) {
|
||||
return {
|
||||
index: function(req, res, next) {
|
||||
host = String(req.headers['x-forwarded-host']);
|
||||
host = host.split(':')[0];
|
||||
|
||||
if (host != 'clock.atlb.co') {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
req.session.redirectUrl = req.url
|
||||
}
|
||||
|
||||
var path = req.path.slice(1);
|
||||
|
||||
res.render('clock.jade', {
|
||||
css: piler.css.renderTags()
|
||||
});
|
||||
},
|
||||
post: function(req, res) {
|
||||
var clock = new Clock({
|
||||
tech: req.user,
|
||||
action: req.body.action,
|
||||
lat: req.body.lat,
|
||||
long: req.body.long,
|
||||
dt: new Date()
|
||||
});
|
||||
|
||||
clock.save(function(err, result) {
|
||||
if (err) {
|
||||
return res.json(500, err);
|
||||
} else {
|
||||
res.json(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
64
app/controllers/db.js
Normal file
64
app/controllers/db.js
Normal file
@ -0,0 +1,64 @@
|
||||
var mongoose = require('mongoose');
|
||||
|
||||
var TimeClockSpan = mongoose.model('TimeClockSpan');
|
||||
var Workorder = mongoose.model('Workorder');
|
||||
|
||||
var db = {};
|
||||
|
||||
db.spans = {
|
||||
forWeek: function(week) {
|
||||
const startOfWeek = week.clone();
|
||||
const endOfWeek = week.clone().endOf('week');
|
||||
|
||||
const query = {
|
||||
start: {
|
||||
$gte: startOfWeek,
|
||||
$lte: endOfWeek
|
||||
}
|
||||
};
|
||||
|
||||
return TimeClockSpan.find(query);
|
||||
},
|
||||
forWeekByUser: function(week, user) {
|
||||
const startOfWeek = week.clone();
|
||||
const endOfWeek = week.clone().endOf('week');
|
||||
|
||||
const query = {
|
||||
start: {
|
||||
$gte: startOfWeek,
|
||||
$lte: endOfWeek
|
||||
},
|
||||
user: user
|
||||
};
|
||||
|
||||
return TimeClockSpan.find(query);
|
||||
}
|
||||
};
|
||||
|
||||
db.workorders = {
|
||||
findByIds: function(ids) {
|
||||
const query = {
|
||||
_id: {
|
||||
$in: ids
|
||||
}
|
||||
};
|
||||
|
||||
return Workorder.find(query);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = db;
|
||||
|
||||
|
||||
function findWorkordersById(ids) {
|
||||
const query = {
|
||||
_id: {
|
||||
$in: ids
|
||||
}
|
||||
};
|
||||
|
||||
return Workorder
|
||||
.find(query)
|
||||
.populate('client', 'name identifier')
|
||||
.exec();
|
||||
}
|
@ -155,9 +155,9 @@ function getPmsByDate(year, month) {
|
||||
];
|
||||
}
|
||||
|
||||
return Workorder.aggregateAsync(pipeline)
|
||||
return Workorder.aggregate(pipeline)
|
||||
.exec()
|
||||
.then(function(pmsData) {
|
||||
|
||||
var data = {};
|
||||
|
||||
if (month !== undefined) {
|
||||
@ -183,12 +183,13 @@ function getPmsByDate(year, month) {
|
||||
}
|
||||
|
||||
function getClients() {
|
||||
return Client.find({ deleted: false })
|
||||
return Client
|
||||
.find({ deleted: false })
|
||||
.lean()
|
||||
.select('name identifier frequencies')
|
||||
.slice('contacts', 1)
|
||||
.sort('name')
|
||||
.execAsync();
|
||||
.exec();
|
||||
}
|
||||
|
||||
function filterClientsByFrequency(month, frequency) {
|
||||
|
@ -5,516 +5,597 @@ var moment = require('moment-timezone');
|
||||
var _ = require('lodash');
|
||||
var Promise = require('bluebird');
|
||||
var TimeClockSpan = mongoose.model('TimeClockSpan');
|
||||
var TimeClockException = mongoose.model('TimeClockException');
|
||||
var Workorder = mongoose.model('Workorder');
|
||||
var email = require('../util/email');
|
||||
var config = require('../../config/config')['prod'];
|
||||
|
||||
var NON_BILLABLES = ['shop', 'break', 'pto', 'meeting', 'event', 'weather', 'holiday'];
|
||||
var TASK_TYPES = ['workday', 'workorder', 'nonBillable'];
|
||||
|
||||
function MultipleSpansError(spans) {
|
||||
Error.captureStackTrace(this, MultipleSpansError);
|
||||
var exceptionTemplate = email.template(
|
||||
'exception.html.tmpl',
|
||||
'exception.text.tmpl',
|
||||
'Exception',
|
||||
[
|
||||
'techName',
|
||||
'date',
|
||||
'message'
|
||||
]
|
||||
);
|
||||
|
||||
this.name = 'MultipleSpansError';
|
||||
this.message = 'Encountered multiple spans when one was expected';
|
||||
this.spans = spans;
|
||||
function MultipleSpansError(spans) {
|
||||
Error.captureStackTrace(this, MultipleSpansError);
|
||||
|
||||
this.name = 'MultipleSpansError';
|
||||
this.message = 'Encountered multiple spans when one was expected';
|
||||
this.spans = spans;
|
||||
}
|
||||
MultipleSpansError.prototype = Object.create(Error.prototype);
|
||||
|
||||
function findUserSpans(user, day) {
|
||||
var startOfDay = day.clone().startOf('day').utc().toDate();
|
||||
var endOfDay = day.clone().endOf('day').utc().toDate();
|
||||
var startOfDay = day.clone().startOf('day').toDate();
|
||||
var endOfDay = day.clone().endOf('day').toDate();
|
||||
|
||||
var query = {
|
||||
start: {
|
||||
'$gte': startOfDay,
|
||||
'$lte': endOfDay
|
||||
},
|
||||
user: user.id
|
||||
};
|
||||
var query = {
|
||||
start: {
|
||||
'$gte': startOfDay,
|
||||
'$lte': endOfDay
|
||||
},
|
||||
user: user
|
||||
};
|
||||
|
||||
return TimeClockSpan
|
||||
.find(query)
|
||||
.exec();
|
||||
return TimeClockSpan
|
||||
.find(query)
|
||||
.exec();
|
||||
}
|
||||
|
||||
function findUserWorkorders(user, day) {
|
||||
var startOfDay = day.clone().startOf('day').toDate();
|
||||
var endOfDay = day.clone().endOf('day').toDate();
|
||||
var startOfDay = day.clone().startOf('day').toDate();
|
||||
var endOfDay = day.clone().endOf('day').toDate();
|
||||
|
||||
var query = {
|
||||
deleted: false,
|
||||
techs: user.id,
|
||||
'scheduling.start': {
|
||||
'$lte': endOfDay
|
||||
},
|
||||
'scheduling.end': {
|
||||
'$gte': startOfDay
|
||||
}
|
||||
};
|
||||
var query = {
|
||||
deleted: false,
|
||||
techs: user.id,
|
||||
'scheduling.start': {
|
||||
'$lte': endOfDay
|
||||
},
|
||||
'scheduling.end': {
|
||||
'$gte': startOfDay
|
||||
}
|
||||
};
|
||||
|
||||
return Workorder
|
||||
.find(query)
|
||||
.populate('client', 'name identifier')
|
||||
.exec();
|
||||
return Workorder
|
||||
.find(query)
|
||||
.populate('client', 'name identifier')
|
||||
.exec();
|
||||
}
|
||||
|
||||
function findUserWorkorder(id, user) {
|
||||
var query = {
|
||||
_id: id,
|
||||
techs: user.id,
|
||||
deleted: false
|
||||
};
|
||||
var query = {
|
||||
_id: id,
|
||||
techs: user.id,
|
||||
deleted: false
|
||||
};
|
||||
|
||||
return Workorder
|
||||
.findOne(query)
|
||||
.populate('client', 'name identifier contacts address')
|
||||
.exec();
|
||||
return Workorder
|
||||
.findOne(query)
|
||||
.populate('client', 'name identifier contacts address')
|
||||
.exec();
|
||||
}
|
||||
|
||||
function filterSpans(spans, filter) {
|
||||
filter = {
|
||||
type: filter.type ? [].concat(filter.type) : undefined,
|
||||
status: filter.status ? String(filter.status) : undefined,
|
||||
workorder: filter.workorder ? String(filter.workorder) : undefined,
|
||||
reason: filter.reason ? String(filter.reason) : undefined
|
||||
};
|
||||
filter = {
|
||||
type: filter.type ? [].concat(filter.type) : undefined,
|
||||
status: filter.status ? String(filter.status) : undefined,
|
||||
workorder: filter.workorder ? String(filter.workorder) : undefined,
|
||||
reason: filter.reason ? String(filter.reason) : undefined
|
||||
};
|
||||
|
||||
return _.chain(spans)
|
||||
.filter(function(span) {
|
||||
return _.chain(spans)
|
||||
.filter(function (span) {
|
||||
|
||||
if (filter.type && filter.type.indexOf(span.type) === -1) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type && filter.type.indexOf(span.type) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.status && String(span.status) !== filter.status) {
|
||||
return false;
|
||||
}
|
||||
if (filter.status && String(span.status) !== filter.status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.workorder && String(span.workorder) !== filter.workorder) {
|
||||
return false;
|
||||
}
|
||||
if (filter.workorder && String(span.workorder) !== filter.workorder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.reason && span.reason !== filter.reason) {
|
||||
return false;
|
||||
}
|
||||
if (filter.reason && span.reason !== filter.reason) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.sortBy('start')
|
||||
.value();
|
||||
return true;
|
||||
})
|
||||
.sortBy('start')
|
||||
.value();
|
||||
}
|
||||
|
||||
function spansStatus(spans, filter) {
|
||||
if (filter) {
|
||||
spans = filterSpans(spans, filter);
|
||||
if (filter) {
|
||||
spans = filterSpans(spans, filter);
|
||||
}
|
||||
|
||||
var result = 'pending';
|
||||
_.forEach(spans, function (span) {
|
||||
if (span.status === 'open') {
|
||||
result = 'clockedIn';
|
||||
return false;
|
||||
} else {
|
||||
result = 'clockedOut';
|
||||
}
|
||||
});
|
||||
|
||||
var result = 'pending';
|
||||
_.forEach(spans, function(span) {
|
||||
if (span.status === 'open') {
|
||||
result = 'clockedIn';
|
||||
return false;
|
||||
} else {
|
||||
result = 'clockedOut';
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateClockRequest(req) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
|
||||
var params = {};
|
||||
var params = {};
|
||||
|
||||
var type = req.body.type;
|
||||
if (!type) {
|
||||
return reject("Missing required parameter 'type'");
|
||||
}
|
||||
var type = req.body.type;
|
||||
if (!type) {
|
||||
return reject("Missing required parameter 'type'");
|
||||
}
|
||||
|
||||
if (TASK_TYPES.indexOf(type) === -1) {
|
||||
return reject("Invalid type: '" + type + "'");
|
||||
}
|
||||
if (TASK_TYPES.indexOf(type) === -1) {
|
||||
return reject("Invalid type: '" + type + "'");
|
||||
}
|
||||
|
||||
params.type = type;
|
||||
params.type = type;
|
||||
|
||||
if (type === 'workorder') {
|
||||
var id = req.body.id;
|
||||
if (!id) {
|
||||
return reject("Missing required parameter 'id'");
|
||||
}
|
||||
if (type === 'workorder') {
|
||||
var id = req.body.id;
|
||||
if (!id) {
|
||||
return reject("Missing required parameter 'id'");
|
||||
}
|
||||
|
||||
params.id = id;
|
||||
}
|
||||
params.id = id;
|
||||
}
|
||||
|
||||
if (type === 'nonBillable') {
|
||||
var reason = req.body.reason;
|
||||
if (!reason) {
|
||||
return reject("Missing required parameter 'reason'");
|
||||
}
|
||||
if (type === 'nonBillable') {
|
||||
var reason = req.body.reason;
|
||||
if (!reason) {
|
||||
return reject("Missing required parameter 'reason'");
|
||||
}
|
||||
|
||||
if (NON_BILLABLES.indexOf(reason) === -1) {
|
||||
return reject("Invalid reason: '" + reason + "'");
|
||||
}
|
||||
if (NON_BILLABLES.indexOf(reason) === -1) {
|
||||
return reject("Invalid reason: '" + reason + "'");
|
||||
}
|
||||
|
||||
params.reason = reason;
|
||||
}
|
||||
params.reason = reason;
|
||||
}
|
||||
|
||||
resolve(params);
|
||||
});
|
||||
resolve(params);
|
||||
});
|
||||
}
|
||||
|
||||
function validateWorkorderDetailsRequest(req) {
|
||||
var params = {};
|
||||
var params = {};
|
||||
|
||||
var id = req.param('id');
|
||||
if (!id) {
|
||||
return Promise.reject("Missing required parameter 'id'");
|
||||
}
|
||||
var id = req.param('id');
|
||||
if (!id) {
|
||||
return Promise.reject("Missing required parameter 'id'");
|
||||
}
|
||||
|
||||
params.id = id;
|
||||
params.id = id;
|
||||
|
||||
return Promise.resolve(params);
|
||||
return Promise.resolve(params);
|
||||
}
|
||||
|
||||
function handleStatusRequest(spans, workorders) {
|
||||
|
||||
var results = {};
|
||||
var results = {};
|
||||
|
||||
var workdaySpans = _.filter(spans, { type: 'workday' });
|
||||
var workdaySpans = _.filter(spans, {type: 'workday'});
|
||||
|
||||
results.tasks = [];
|
||||
results.tasks = [];
|
||||
|
||||
results.tasks = results.tasks.concat({
|
||||
type: 'workday',
|
||||
status: spansStatus(workdaySpans),
|
||||
spans: _.map(workdaySpans, spanToResponse)
|
||||
});
|
||||
results.tasks = results.tasks.concat({
|
||||
type: 'workday',
|
||||
status: spansStatus(workdaySpans),
|
||||
spans: _.map(workdaySpans, spanToResponse)
|
||||
});
|
||||
|
||||
results.tasks = results.tasks.concat(_.chain(workorders)
|
||||
.sortBy('scheduling.start')
|
||||
.map(function(workorder) {
|
||||
var workorderSpans = filterSpans(spans, { type: 'workorder', workorder: workorder.id });
|
||||
results.tasks = results.tasks.concat(_.chain(workorders)
|
||||
.sortBy('scheduling.start')
|
||||
.map(function (workorder) {
|
||||
var workorderSpans = filterSpans(spans, {type: 'workorder', workorder: workorder.id});
|
||||
|
||||
return {
|
||||
type: 'workorder',
|
||||
id: workorder.id,
|
||||
title: workorder.client.name,
|
||||
start: moment(workorder.scheduling.start).utc().toISOString(),
|
||||
end: moment(workorder.scheduling.end).utc().toISOString(),
|
||||
status: spansStatus(workorderSpans),
|
||||
spans: _.map(workorderSpans, spanToResponse)
|
||||
};
|
||||
})
|
||||
.value()
|
||||
);
|
||||
return {
|
||||
type: 'workorder',
|
||||
id: workorder.id,
|
||||
title: workorder.client.name,
|
||||
start: moment(workorder.scheduling.start).utc().toISOString(),
|
||||
end: moment(workorder.scheduling.end).utc().toISOString(),
|
||||
status: spansStatus(workorderSpans),
|
||||
spans: _.map(workorderSpans, spanToResponse)
|
||||
};
|
||||
})
|
||||
.value()
|
||||
);
|
||||
|
||||
results.tasks = results.tasks.concat(_.chain(NON_BILLABLES)
|
||||
.map(function(nonBillable) {
|
||||
var nonBillableSpans = filterSpans(spans, { reason: nonBillable });
|
||||
results.tasks = results.tasks.concat(_.chain(NON_BILLABLES)
|
||||
.map(function (nonBillable) {
|
||||
var nonBillableSpans = filterSpans(spans, {reason: nonBillable});
|
||||
|
||||
return {
|
||||
type: 'nonBillable',
|
||||
reason: nonBillable,
|
||||
status: spansStatus(nonBillableSpans),
|
||||
spans: _.map(nonBillableSpans, spanToResponse)
|
||||
};
|
||||
})
|
||||
.value()
|
||||
);
|
||||
return {
|
||||
type: 'nonBillable',
|
||||
reason: nonBillable,
|
||||
status: spansStatus(nonBillableSpans),
|
||||
spans: _.map(nonBillableSpans, spanToResponse)
|
||||
};
|
||||
})
|
||||
.value()
|
||||
);
|
||||
|
||||
return results;
|
||||
return results;
|
||||
}
|
||||
|
||||
function handleClockInRequest(params, user, spans, workorders, now) {
|
||||
var workdayStatus = spansStatus(spans, { type: 'workday' });
|
||||
var workdayStatus = spansStatus(spans, {type: 'workday'});
|
||||
|
||||
if (params.type === 'workday') {
|
||||
if (workdayStatus === 'clockedIn') {
|
||||
return Promise.reject('Already clocked in');
|
||||
}
|
||||
|
||||
var span = new TimeClockSpan({
|
||||
user: user.id,
|
||||
type: 'workday',
|
||||
status: 'open',
|
||||
start: now.clone().utc().toDate()
|
||||
});
|
||||
|
||||
} else {
|
||||
if (workdayStatus !== 'clockedIn') {
|
||||
return Promise.reject('Not clocked into day');
|
||||
}
|
||||
|
||||
var allTasksStatus = spansStatus(spans, {type: ['workorder', 'nonBillable']});
|
||||
if (allTasksStatus === 'clockedIn') {
|
||||
return Promise.reject('Already clocked in');
|
||||
}
|
||||
|
||||
if (params.type === 'workorder') {
|
||||
var workorder = _.find(workorders, {id: params.id});
|
||||
if (!workorder) {
|
||||
return Promise.reject('Invalid workorder');
|
||||
}
|
||||
|
||||
handleClockInExceptions(user, workorder, spans, now);
|
||||
|
||||
var span = new TimeClockSpan({
|
||||
user: user.id,
|
||||
type: 'workorder',
|
||||
status: 'open',
|
||||
start: now.clone().utc().toDate(),
|
||||
workorder: workorder.id,
|
||||
client: workorder.client.id
|
||||
});
|
||||
}
|
||||
|
||||
if (params.type === 'nonBillable') {
|
||||
var span = new TimeClockSpan({
|
||||
user: user.id,
|
||||
type: 'nonBillable',
|
||||
status: 'open',
|
||||
start: now.clone().utc().toDate(),
|
||||
reason: params.reason
|
||||
});
|
||||
}
|
||||
if (params.type === 'workday') {
|
||||
if (workdayStatus === 'clockedIn') {
|
||||
return Promise.reject('Already clocked in');
|
||||
}
|
||||
|
||||
return span.save().then(spanToResponse);
|
||||
var span = new TimeClockSpan({
|
||||
user: user.id,
|
||||
type: 'workday',
|
||||
status: 'open',
|
||||
start: now.clone().utc().toDate()
|
||||
});
|
||||
|
||||
} else {
|
||||
if (workdayStatus !== 'clockedIn') {
|
||||
return Promise.reject('Not clocked into day');
|
||||
}
|
||||
|
||||
var allTasksStatus = spansStatus(spans, {type: ['workorder', 'nonBillable']});
|
||||
if (allTasksStatus === 'clockedIn') {
|
||||
return Promise.reject('Already clocked in');
|
||||
}
|
||||
|
||||
if (params.type === 'workorder') {
|
||||
var workorder = _.find(workorders, {id: params.id});
|
||||
if (!workorder) {
|
||||
return Promise.reject('Invalid workorder');
|
||||
}
|
||||
|
||||
handleClockInExceptions(user, workorder, spans, now);
|
||||
|
||||
var span = new TimeClockSpan({
|
||||
user: user.id,
|
||||
type: 'workorder',
|
||||
status: 'open',
|
||||
start: now.clone().utc().toDate(),
|
||||
workorder: workorder.id,
|
||||
client: workorder.client.id
|
||||
});
|
||||
}
|
||||
|
||||
if (params.type === 'nonBillable') {
|
||||
var span = new TimeClockSpan({
|
||||
user: user.id,
|
||||
type: 'nonBillable',
|
||||
status: 'open',
|
||||
start: now.clone().utc().toDate(),
|
||||
reason: params.reason
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return span.save().then(spanToResponse);
|
||||
}
|
||||
|
||||
function handleClockInExceptions(user, workorder, spans, now) {
|
||||
|
||||
var closedWorkordersSpans = filterSpans(spans, { type: 'workorder', status: 'closed' });
|
||||
var isFirstWorkorder = closedWorkordersSpans.length == 0;
|
||||
var closedWorkordersSpans = filterSpans(spans, {type: 'workorder', status: 'closed'});
|
||||
var isFirstWorkorder = closedWorkordersSpans.length == 0;
|
||||
|
||||
if (isFirstWorkorder) {
|
||||
var start = moment(workorder.scheduling.start);
|
||||
var minutes = now.diff(start, 'minutes');
|
||||
if (isFirstWorkorder) {
|
||||
var start = moment(workorder.scheduling.start);
|
||||
var minutes = now.diff(start, 'minutes');
|
||||
|
||||
if (minutes > 15) {
|
||||
reportException({
|
||||
user: user,
|
||||
workorder: workorder,
|
||||
reason: 'User is late to first workorder.'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var previousWorkorderSpan = _.last(closedWorkordersSpans);
|
||||
if (minutes > 15) {
|
||||
|
||||
if (previousWorkorderSpan.workorder != workorder.id) {
|
||||
console.log(previousWorkorderSpan);
|
||||
new TimeClockException({
|
||||
user: user._id,
|
||||
date: new Date(),
|
||||
reason: 'late_to_first_workorder'
|
||||
}).save();
|
||||
|
||||
var end = moment(previousWorkorderSpan.end);
|
||||
var minutes = now.diff(end, 'minutes');
|
||||
console.log("Time between tasks: ", minutes);
|
||||
|
||||
if (minutes < 5) {
|
||||
reportException({
|
||||
user: user,
|
||||
workorder: workorder,
|
||||
reason: 'User clocked in to next workorder too quickly.'
|
||||
});
|
||||
}
|
||||
|
||||
if (minutes > 75) {
|
||||
reportException({
|
||||
user: user,
|
||||
workorder: workorder,
|
||||
reason: 'Too much travel time detected between jobs.'
|
||||
});
|
||||
}
|
||||
}
|
||||
reportException({
|
||||
user: user,
|
||||
workorder: workorder,
|
||||
reason: 'User is late to first workorder.'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var previousWorkorderSpan = _.last(closedWorkordersSpans);
|
||||
|
||||
if (previousWorkorderSpan.workorder != workorder.id) {
|
||||
console.log(previousWorkorderSpan);
|
||||
|
||||
var end = moment(previousWorkorderSpan.end);
|
||||
var minutes = now.diff(end, 'minutes');
|
||||
console.log("Time between tasks: ", minutes);
|
||||
|
||||
if (minutes < 5) {
|
||||
new TimeClockException({
|
||||
user: user._id,
|
||||
date: new Date(),
|
||||
reason: 'too_little_travel'
|
||||
}).save();
|
||||
|
||||
reportException({
|
||||
user: user,
|
||||
workorder: workorder,
|
||||
reason: 'User clocked in to next workorder too quickly.'
|
||||
});
|
||||
}
|
||||
|
||||
if (minutes > 75) {
|
||||
new TimeClockException({
|
||||
user: user._id,
|
||||
date: new Date(),
|
||||
reason: 'too_much_travel'
|
||||
}).save();
|
||||
|
||||
reportException({
|
||||
user: user,
|
||||
workorder: workorder,
|
||||
reason: 'Too much travel time detected between jobs.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reportException(exception) {
|
||||
// TODO: Actually send emails for exceptions.
|
||||
|
||||
console.log('--- EXCEPTION ---', exception.reason);
|
||||
const message = {
|
||||
to: config.email.exception
|
||||
};
|
||||
|
||||
const values = {
|
||||
techName: `${exception.user.name.first} ${exception.user.name.last}`,
|
||||
date: moment().format('LLLL'),
|
||||
message: exception.reason
|
||||
};
|
||||
|
||||
email.send(message, exceptionTemplate, values);
|
||||
|
||||
console.log('--- EXCEPTION ---', new Date(), exception.reason);
|
||||
}
|
||||
|
||||
function handleClockOutRequest(params, user, spans, workorders, now) {
|
||||
var workdaySpans = filterSpans(spans, { type: 'workday' });
|
||||
var workdayStatus = spansStatus(workdaySpans);
|
||||
var workdaySpans = filterSpans(spans, {type: 'workday'});
|
||||
var workdayStatus = spansStatus(workdaySpans);
|
||||
|
||||
if (workdayStatus !== 'clockedIn') {
|
||||
return Promise.reject('Not clocked in');
|
||||
if (workdayStatus !== 'clockedIn') {
|
||||
return Promise.reject('Not clocked in');
|
||||
}
|
||||
|
||||
if (params.type === 'workday') {
|
||||
var allTasksStatus = spansStatus(spans, {type: ['workorder', 'nonBillable']});
|
||||
if (allTasksStatus === 'clockedIn') {
|
||||
return Promise.reject('Cannot clock out while tasks are still open.');
|
||||
}
|
||||
|
||||
if (params.type === 'workday') {
|
||||
var allTasksStatus = spansStatus(spans, {type: ['workorder', 'nonBillable']});
|
||||
if (allTasksStatus === 'clockedIn') {
|
||||
return Promise.reject('Cannot clock out while tasks are still open.');
|
||||
}
|
||||
var span = ensureSingularSpan(filterSpans(workdaySpans, {status: 'open'}));
|
||||
}
|
||||
|
||||
var span = ensureSingularSpan(filterSpans(workdaySpans, {status: 'open'}));
|
||||
if (params.type === 'workorder') {
|
||||
var workorder = _.find(workorders, {id: params.id});
|
||||
if (!workorder) {
|
||||
return Promise.reject('Invalid workorder');
|
||||
}
|
||||
|
||||
if (params.type === 'workorder') {
|
||||
var workorder = _.find(workorders, {id: params.id});
|
||||
if (!workorder) {
|
||||
return Promise.reject('Invalid workorder');
|
||||
}
|
||||
var workorderSpans = filterSpans(spans, {type: 'workorder', workorder: workorder.id});
|
||||
var workorderStatus = spansStatus(workorderSpans);
|
||||
|
||||
var workorderSpans = filterSpans(spans, { type: 'workorder', workorder: workorder.id });
|
||||
var workorderStatus = spansStatus(workorderSpans);
|
||||
|
||||
if (workorderStatus !== 'clockedIn') {
|
||||
return Promise.reject('Not clocked in');
|
||||
}
|
||||
|
||||
var span = ensureSingularSpan(filterSpans(workorderSpans, {status: 'open'}));
|
||||
if (workorderStatus !== 'clockedIn') {
|
||||
return Promise.reject('Not clocked in');
|
||||
}
|
||||
|
||||
if (params.type === 'nonBillable') {
|
||||
var nonBillableSpans = filterSpans(spans, { type: 'nonBillable', reason: params.reason });
|
||||
var nonBillableStatus = spansStatus(nonBillableSpans);
|
||||
var span = ensureSingularSpan(filterSpans(workorderSpans, {status: 'open'}));
|
||||
}
|
||||
|
||||
if (nonBillableStatus !== 'clockedIn') {
|
||||
return Promise.reject('Not clocked in');
|
||||
}
|
||||
if (params.type === 'nonBillable') {
|
||||
var nonBillableSpans = filterSpans(spans, {type: 'nonBillable', reason: params.reason});
|
||||
var nonBillableStatus = spansStatus(nonBillableSpans);
|
||||
|
||||
var span = ensureSingularSpan(filterSpans(nonBillableSpans, {status: 'open'}));
|
||||
if (nonBillableStatus !== 'clockedIn') {
|
||||
return Promise.reject('Not clocked in');
|
||||
}
|
||||
|
||||
span.status = 'closed';
|
||||
span.end = now.clone().utc().toDate();
|
||||
span.duration = moment(span.end).diff(span.start, 'seconds');
|
||||
var span = ensureSingularSpan(filterSpans(nonBillableSpans, {status: 'open'}));
|
||||
}
|
||||
|
||||
return span.save().then(spanToResponse);
|
||||
span.status = 'closed';
|
||||
span.end = now.clone().utc().toDate();
|
||||
span.duration = moment(span.end).diff(span.start, 'seconds');
|
||||
|
||||
return span.save().then(spanToResponse);
|
||||
}
|
||||
|
||||
function handleWorkorderDetailsRequest(params, user, spans, workorder, today) {
|
||||
if (!workorder) {
|
||||
return Promise.reject('Invalid workorder');
|
||||
if (!workorder) {
|
||||
return Promise.reject('Invalid workorder');
|
||||
}
|
||||
|
||||
var workorderSpans = filterSpans(spans, {type: 'workorder', workorder: workorder.id});
|
||||
var workorderStatus = spansStatus(workorderSpans);
|
||||
|
||||
workorder = workorder.toObject();
|
||||
workorder.timeclock = {
|
||||
type: 'workorder',
|
||||
id: workorder._id,
|
||||
title: workorder.client.name,
|
||||
start: moment(workorder.scheduling.start).utc().toISOString(),
|
||||
end: moment(workorder.scheduling.end).utc().toISOString(),
|
||||
status: workorderStatus,
|
||||
spans: _.map(workorderSpans, spanToResponse),
|
||||
blocked: false
|
||||
};
|
||||
|
||||
if (workorderStatus != 'clockedIn') {
|
||||
var workdayStatus = spansStatus(spans, {type: 'workday'});
|
||||
var otherSpansStatus = spansStatus(spans, {type: ['workorder', 'nonBillable']});
|
||||
|
||||
if (workdayStatus != 'clockedIn' || otherSpansStatus == 'clockedIn') {
|
||||
workorder.timeclock.blocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
var workorderSpans = filterSpans(spans, { type: 'workorder', workorder: workorder.id });
|
||||
var workorderStatus = spansStatus(workorderSpans);
|
||||
|
||||
workorder = workorder.toObject();
|
||||
workorder.timeclock = {
|
||||
type: 'workorder',
|
||||
id: workorder._id,
|
||||
title: workorder.client.name,
|
||||
start: moment(workorder.scheduling.start).utc().toISOString(),
|
||||
end: moment(workorder.scheduling.end).utc().toISOString(),
|
||||
status: workorderStatus,
|
||||
spans: _.map(workorderSpans, spanToResponse),
|
||||
blocked: false
|
||||
};
|
||||
|
||||
if (workorderStatus != 'clockedIn') {
|
||||
var workdayStatus = spansStatus(spans, { type: 'workday' });
|
||||
var otherSpansStatus = spansStatus(spans, {type: ['workorder', 'nonBillable']});
|
||||
|
||||
if (workdayStatus != 'clockedIn' || otherSpansStatus == 'clockedIn') {
|
||||
workorder.timeclock.blocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
return workorder;
|
||||
return workorder;
|
||||
}
|
||||
|
||||
function spanToResponse(span) {
|
||||
return {
|
||||
start: span.start,
|
||||
end: span.end,
|
||||
duration: span.duration
|
||||
};
|
||||
return {
|
||||
start: span.start,
|
||||
end: span.end,
|
||||
duration: span.duration
|
||||
};
|
||||
}
|
||||
|
||||
function ensureSingularSpan(spans) {
|
||||
if (spans.length != 1) {
|
||||
throw new MultipleSpansError(spans);
|
||||
}
|
||||
if (spans.length != 1) {
|
||||
throw new MultipleSpansError(spans);
|
||||
}
|
||||
|
||||
return spans[0];
|
||||
return spans[0];
|
||||
}
|
||||
|
||||
function responseHandler(res) {
|
||||
return function(data) {
|
||||
res.json(data);
|
||||
};
|
||||
return function (data) {
|
||||
res.json(data);
|
||||
};
|
||||
}
|
||||
|
||||
function errorHandler(res) {
|
||||
return function(error) {
|
||||
if (typeof error === 'string') {
|
||||
res.json(400, {
|
||||
error: {
|
||||
message: error
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error(error.stack);
|
||||
res.json(500, 'Internal error');
|
||||
return function (error) {
|
||||
if (typeof error === 'string') {
|
||||
res.json(400, {
|
||||
error: {
|
||||
message: error
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
console.error(error.stack);
|
||||
res.json(500, 'Internal error');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
return {
|
||||
index: function(req, res) {
|
||||
function validateUserId(req) {
|
||||
const id = req.param('user_id');
|
||||
if (!id) {
|
||||
return Promise.reject("Parameter missing 'user_id'");
|
||||
}
|
||||
|
||||
//TODO: Check to make sure user has a valid timesheet.
|
||||
return Promise.resolve(id);
|
||||
}
|
||||
|
||||
var today = moment();
|
||||
function validateDate(req, field) {
|
||||
let date = req.param(field);
|
||||
|
||||
var spans = findUserSpans(req.user, today);
|
||||
var workorders = findUserWorkorders(req.user, today);
|
||||
if (!date) {
|
||||
return Promise.reject(`Parameter '${field}' is required.`);
|
||||
}
|
||||
|
||||
Promise.join(spans, workorders, handleStatusRequest)
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
date = moment(date, 'YYYY-MM-DD');
|
||||
if (!date.isValid()) {
|
||||
return Promise.reject(`Parameter '${field}' is not a valid date.`)
|
||||
}
|
||||
|
||||
clockIn: function(req, res) {
|
||||
return Promise.resolve(date);
|
||||
}
|
||||
|
||||
//TODO: Check to make sure user has a valid timesheet.
|
||||
module.exports = function () {
|
||||
return {
|
||||
index: function (req, res) {
|
||||
|
||||
var today = moment();
|
||||
//TODO: Check to make sure user has a valid timesheet.
|
||||
|
||||
var params = validateClockRequest(req);
|
||||
var spans = findUserSpans(req.user, today);
|
||||
var workorders = findUserWorkorders(req.user, today);
|
||||
var today = moment();
|
||||
|
||||
Promise.join(params, req.user, spans, workorders, today, handleClockInRequest)
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
var spans = findUserSpans(req.user.id, today);
|
||||
var workorders = findUserWorkorders(req.user, today);
|
||||
|
||||
clockOut: function(req, res) {
|
||||
Promise.join(spans, workorders, handleStatusRequest)
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
|
||||
//TODO: Check to make sure user has a valid timesheet.
|
||||
clockIn: function (req, res) {
|
||||
|
||||
var today = moment();
|
||||
//TODO: Check to make sure user has a valid timesheet.
|
||||
|
||||
var params = validateClockRequest(req);
|
||||
var spans = findUserSpans(req.user, today);
|
||||
var workorders = findUserWorkorders(req.user, today);
|
||||
var today = moment();
|
||||
|
||||
Promise.join(params, req.user, spans, workorders, today, handleClockOutRequest)
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
var params = validateClockRequest(req);
|
||||
var spans = findUserSpans(req.user.id, today);
|
||||
var workorders = findUserWorkorders(req.user, today);
|
||||
|
||||
workorderDetails: function(req, res) {
|
||||
Promise.join(params, req.user, spans, workorders, today, handleClockInRequest)
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
|
||||
var today = moment();
|
||||
clockOut: function (req, res) {
|
||||
|
||||
validateWorkorderDetailsRequest(req)
|
||||
.then(function(params) {
|
||||
var spans = findUserSpans(req.user, today);
|
||||
var workorder = findUserWorkorder(params.id, req.user);
|
||||
//TODO: Check to make sure user has a valid timesheet.
|
||||
|
||||
return Promise.join(params, req.user, spans, workorder, today, handleWorkorderDetailsRequest)
|
||||
})
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
Promise
|
||||
.props({
|
||||
id: req.user.id,
|
||||
date: moment(),
|
||||
notes: req.body.notes
|
||||
})
|
||||
.then((params) => {
|
||||
var spans = findUserSpans(req.user.id, params.date);
|
||||
var workorders = findUserWorkorders(req.user, params.date);
|
||||
return Promise.join(params, req.user, spans, workorders, params.date, handleClockOutRequest);
|
||||
})
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
|
||||
spansForUser: function (req, res) {
|
||||
Promise
|
||||
.props({
|
||||
id: validateUserId(req),
|
||||
date: validateDate(req, 'date')
|
||||
})
|
||||
.then((params) => findUserSpans(params.id, params.date))
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
|
||||
Promise.join
|
||||
}
|
||||
}
|
||||
workorderDetails: function (req, res) {
|
||||
|
||||
var today = moment();
|
||||
|
||||
validateWorkorderDetailsRequest(req)
|
||||
.then(function (params) {
|
||||
var spans = findUserSpans(req.user, today);
|
||||
var workorder = findUserWorkorder(params.id, req.user);
|
||||
|
||||
return Promise.join(params, req.user, spans, workorder, today, handleWorkorderDetailsRequest)
|
||||
})
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
|
370
app/controllers/timesheet.js
Normal file
370
app/controllers/timesheet.js
Normal file
@ -0,0 +1,370 @@
|
||||
"use strict";
|
||||
|
||||
var mongoose = require('mongoose');
|
||||
var moment = require('moment-timezone');
|
||||
var _ = require('lodash');
|
||||
var Promise = require('bluebird');
|
||||
var TimeClockSpan = mongoose.model('TimeClockSpan');
|
||||
var Workorder = mongoose.model('Workorder');
|
||||
var User = mongoose.model('User');
|
||||
var db = require('./db');
|
||||
|
||||
const NON_BILLABLE_WORKORDER_TYPES = [
|
||||
'shop', 'break', 'pto', 'meeting', 'event', 'weather'
|
||||
];
|
||||
|
||||
const PAYROLL_WORKORDER_TYPE_MAP = {
|
||||
'office': 'office',
|
||||
'anesthesia': 'anesthesia',
|
||||
'biomed': 'biomed',
|
||||
'bsp': 'bsp',
|
||||
'ice': 'ice',
|
||||
'imaging': 'other',
|
||||
'sales': 'sales',
|
||||
'sterile-processing': 'sterilizer',
|
||||
'depot': 'depot',
|
||||
'trace-gas': 'other',
|
||||
'room-air-exchange': 'other',
|
||||
'isolation-panels': 'electric',
|
||||
'ups-systems': 'electric',
|
||||
'relocation': 'other',
|
||||
'ice-maker': 'other',
|
||||
'waste-management-system': 'other',
|
||||
'medgas': 'other',
|
||||
'staffing': 'other',
|
||||
'ert': 'electric',
|
||||
'shop': 'non-billable',
|
||||
'break': 'non-billable',
|
||||
'pto': 'non-billable',
|
||||
'meeting': 'non-billable',
|
||||
'event': 'non-billable',
|
||||
'weather': 'non-billable',
|
||||
'legacy': 'legacy'
|
||||
};
|
||||
|
||||
|
||||
function findUserDaysWorked(id) {
|
||||
var query = {
|
||||
user: id,
|
||||
type: 'workday',
|
||||
status: 'closed'
|
||||
};
|
||||
|
||||
return TimeClockSpan
|
||||
.find(query).exec()
|
||||
.then((records) => _.chain(records).reduce(accumulateDaysWorked, {}).values())
|
||||
}
|
||||
|
||||
function accumulateDaysWorked(result, record) {
|
||||
const date = moment(record.start).local().startOf('day').format('YYYY-MM-DD');
|
||||
|
||||
if (!result[date]) {
|
||||
result[date] = {
|
||||
date: date,
|
||||
duration: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (record.duration) {
|
||||
result[date].duration += record.duration;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function responseHandler(res) {
|
||||
return function (data) {
|
||||
res.json(data);
|
||||
};
|
||||
}
|
||||
|
||||
function errorHandler(res) {
|
||||
return function (error) {
|
||||
if (typeof error === 'string') {
|
||||
res.json(400, {
|
||||
error: {
|
||||
message: error
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error(error.stack);
|
||||
res.json(500, 'Internal error');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function validateUserId(req) {
|
||||
const id = req.param('user_id');
|
||||
if (!id) {
|
||||
return Promise.reject("Parameter missing 'user_id'");
|
||||
}
|
||||
|
||||
return Promise.resolve(id);
|
||||
}
|
||||
|
||||
function validateWeek(req) {
|
||||
let week = req.param('week');
|
||||
|
||||
if (!week) {
|
||||
return Promise.reject("Parameter 'week' is required.");
|
||||
}
|
||||
|
||||
week = moment(week, 'YYYY-MM-DD');
|
||||
if (!week.isValid()) {
|
||||
return Promise.reject("Parameter 'week' is not a valid date.")
|
||||
}
|
||||
|
||||
if (week.weekday() !== 0) {
|
||||
return Promise.reject("Parameter 'week' does not start at the beginning of the week (Sunday).");
|
||||
}
|
||||
|
||||
// Return as string.
|
||||
return Promise.resolve(week);
|
||||
}
|
||||
|
||||
function summaryHandler(params) {
|
||||
const spans = findAllSpansForWeek(params.week);
|
||||
|
||||
return buildReport(spans);
|
||||
}
|
||||
|
||||
function userSummaryHandler(params) {
|
||||
const spans = findUserSpansForWeek(params.id, params.week);
|
||||
|
||||
return buildReport(spans);
|
||||
}
|
||||
|
||||
function buildReport(spans) {
|
||||
const workordersById = spans
|
||||
.then(extractIds('workorder'))
|
||||
.then(findWorkordersById)
|
||||
.then(indexById);
|
||||
|
||||
const usersById = spans
|
||||
.then(extractIds('user'))
|
||||
.then(findUsersById)
|
||||
.then(indexById);
|
||||
|
||||
return Promise.join(spans, workordersById, usersById, generateSummary);
|
||||
}
|
||||
|
||||
function generateSummary(spans, workordersById, usersById) {
|
||||
var results = {};
|
||||
|
||||
function fetchOrCreateUserRecord(userId) {
|
||||
var record = results[userId];
|
||||
if (!record) {
|
||||
var user = usersById[userId];
|
||||
|
||||
record = results[userId] = {
|
||||
user: {
|
||||
_id: user._id,
|
||||
name: user.name
|
||||
},
|
||||
hasOpenSpans: false,
|
||||
workorders: {},
|
||||
spans: {},
|
||||
clockedTime: 0,
|
||||
workedTime: 0,
|
||||
accountingByWorkorder: {},
|
||||
accountingByWorkorderType: {},
|
||||
accountingByPayroll: {},
|
||||
accountingByNonBillable: {},
|
||||
};
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
function addWorkorder(user, workorder) {
|
||||
if (!user.workorders[workorder.id]) {
|
||||
user.workorders[workorder.id] = {
|
||||
_id: workorder._id,
|
||||
client: workorder.client,
|
||||
biomedId: workorder.biomedId,
|
||||
reason: workorder.reason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logTime(collection, key, duration) {
|
||||
if (!collection[key]) {
|
||||
collection[key] = {
|
||||
type: key,
|
||||
duration: duration
|
||||
}
|
||||
} else {
|
||||
collection[key].duration += duration;
|
||||
}
|
||||
}
|
||||
|
||||
_.forEach(spans, (span) => {
|
||||
var user = fetchOrCreateUserRecord(span.user);
|
||||
|
||||
user.spans[span._id] = span.toObject();
|
||||
delete user.spans[span._id].__v;
|
||||
delete user.spans[span._id].user;
|
||||
|
||||
if (span.status !== 'closed') {
|
||||
user.hasOpenSpans = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (span.type === 'workday') {
|
||||
user.clockedTime += span.duration;
|
||||
}
|
||||
|
||||
if (span.type === 'workorder') {
|
||||
user.workedTime += span.duration;
|
||||
|
||||
var workorder = workordersById[span.workorder];
|
||||
var workorderType = workorder.workorderType;
|
||||
|
||||
// If workorder is actually a non-billable (Stupid), treat it as such...
|
||||
if (NON_BILLABLE_WORKORDER_TYPES.indexOf(workorderType) > -1) {
|
||||
logTime(user.accountingByNonBillable, workorderType, span.duration);
|
||||
} else {
|
||||
addWorkorder(user, workorder);
|
||||
|
||||
logTime(user.accountingByWorkorderType, workorderType, span.duration);
|
||||
logTime(user.accountingByPayroll, PAYROLL_WORKORDER_TYPE_MAP[workorderType], span.duration);
|
||||
logTime(user.accountingByWorkorder, span.workorder, span.duration);
|
||||
}
|
||||
}
|
||||
|
||||
if (span.type === 'nonBillable') {
|
||||
user.workedTime += span.duration;
|
||||
|
||||
logTime(user.accountingByNonBillable, span.reason, span.duration);
|
||||
}
|
||||
});
|
||||
|
||||
_.forEach(results, (user) => {
|
||||
user.travelTime = Math.max(0, user.clockedTime - user.workedTime);
|
||||
|
||||
user.spans = _.values(user.spans);
|
||||
|
||||
user.accountingByWorkorder = _.values(user.accountingByWorkorder);
|
||||
user.accountingByWorkorderType = _.values(user.accountingByWorkorderType);
|
||||
user.accountingByPayroll = _.values(user.accountingByPayroll);
|
||||
user.accountingByNonBillable = _.values(user.accountingByNonBillable);
|
||||
});
|
||||
|
||||
return _.values(results);
|
||||
}
|
||||
|
||||
function extractIds(field) {
|
||||
return (data) => _(data)
|
||||
.pluck(field)
|
||||
.reject(_.isUndefined)
|
||||
.uniq((id) => id.toString())
|
||||
.value();
|
||||
}
|
||||
|
||||
function indexById(data) {
|
||||
return _.indexBy(data, 'id')
|
||||
}
|
||||
|
||||
function findWorkordersById(ids) {
|
||||
const query = {
|
||||
_id: {
|
||||
$in: ids
|
||||
}
|
||||
};
|
||||
|
||||
return Workorder
|
||||
.find(query)
|
||||
.populate('client', 'name identifier')
|
||||
.exec();
|
||||
}
|
||||
|
||||
function findUsersById(ids) {
|
||||
const query = {
|
||||
_id: {
|
||||
$in: ids
|
||||
}
|
||||
};
|
||||
|
||||
return User.find(query).exec();
|
||||
}
|
||||
|
||||
function findAllSpansForWeek(week) {
|
||||
var startOfWeek = week.clone().startOf('week');
|
||||
var endOfWeek = week.clone().endOf('week');
|
||||
|
||||
console.log(`Finding spans between ${startOfWeek.format()} and ${endOfWeek.format()}`);
|
||||
|
||||
var query = {
|
||||
start: {
|
||||
'$gte': startOfWeek.toDate(),
|
||||
'$lte': endOfWeek.toDate()
|
||||
}
|
||||
};
|
||||
|
||||
return TimeClockSpan.find(query).exec();
|
||||
}
|
||||
|
||||
function findUserSpansForWeek(id, week) {
|
||||
var startOfWeek = week.clone().startOf('week');
|
||||
var endOfWeek = week.clone().endOf('week');
|
||||
|
||||
console.log(`Finding spans between ${startOfWeek.format()} and ${endOfWeek.format()}`);
|
||||
|
||||
var query = {
|
||||
start: {
|
||||
'$gte': startOfWeek.toDate(),
|
||||
'$lte': endOfWeek.toDate()
|
||||
},
|
||||
user: id
|
||||
};
|
||||
|
||||
return TimeClockSpan.find(query).exec();
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
return {
|
||||
daysWorked: function (req, res) {
|
||||
req.check('user_id').notEmpty().isMongoId();
|
||||
|
||||
var errors = req.validationErrors();
|
||||
if (errors) {
|
||||
return res.json(400, errors);
|
||||
}
|
||||
|
||||
var params = {
|
||||
id: req.param('user_id')
|
||||
};
|
||||
|
||||
findUserDaysWorked(params.id)
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
|
||||
summary: function (req, res) {
|
||||
req.check('week').notEmpty().isWeek();
|
||||
|
||||
var errors = req.validationErrors();
|
||||
if (errors) {
|
||||
return res.json(400, errors);
|
||||
}
|
||||
|
||||
var params = {
|
||||
week: moment(req.sanitize('week'))
|
||||
};
|
||||
|
||||
summaryHandler(params)
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
},
|
||||
|
||||
userSummary: function (req, res) {
|
||||
Promise
|
||||
.props({
|
||||
id: validateUserId(req),
|
||||
week: validateWeek(req)
|
||||
})
|
||||
.then(userSummaryHandler)
|
||||
.then(responseHandler(res))
|
||||
.catch(errorHandler(res));
|
||||
}
|
||||
}
|
||||
};
|
@ -1,209 +1,226 @@
|
||||
|
||||
var mongoose = require('mongoose'),
|
||||
async = require('async'),
|
||||
User = mongoose.model('User'),
|
||||
Clock = mongoose.model('Clock');
|
||||
async = require('async'),
|
||||
User = mongoose.model('User'),
|
||||
Clock = mongoose.model('Clock');
|
||||
|
||||
var log = require('log4node');
|
||||
|
||||
module.exports = function(config, directory) {
|
||||
module.exports = function (config, directory) {
|
||||
|
||||
function fetch_all_users(callback) {
|
||||
async.parallel({
|
||||
gapps: directory.listUsers,
|
||||
local: function(callback) {
|
||||
User.find({ deleted: false }).select('name email groups perms deleted').exec(callback);
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
function fetch_all_users(callback) {
|
||||
async.parallel({
|
||||
gapps: directory.listUsers,
|
||||
local: function (callback) {
|
||||
User.find({deleted: false}).select('name email groups perms deleted').exec(callback);
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function map_local_users(data, results) {
|
||||
return function(callback) {
|
||||
async.each(data, function(item, callback) {
|
||||
var key = item.email.toLowerCase();
|
||||
function map_local_users(data, results) {
|
||||
return function (callback) {
|
||||
async.each(data, function (item, callback) {
|
||||
var key = item.email.toLowerCase();
|
||||
|
||||
if (blacklist.indexOf(key) == -1)
|
||||
results[key] = item;
|
||||
if (blacklist.indexOf(key) == -1)
|
||||
results[key] = item;
|
||||
|
||||
callback();
|
||||
},
|
||||
callback);
|
||||
};
|
||||
}
|
||||
callback();
|
||||
},
|
||||
callback);
|
||||
};
|
||||
}
|
||||
|
||||
function map_gapps_users(data, results) {
|
||||
return function(callback) {
|
||||
async.each(data, function(item, callback) {
|
||||
var key = item.primaryEmail.toLowerCase();
|
||||
function map_gapps_users(data, results) {
|
||||
return function (callback) {
|
||||
async.each(data, function (item, callback) {
|
||||
var key = item.primaryEmail.toLowerCase();
|
||||
|
||||
// Ignore if blacklisted
|
||||
if (blacklist.indexOf(key) != -1) return callback();
|
||||
// Ignore if blacklisted
|
||||
if (blacklist.indexOf(key) != -1) return callback();
|
||||
|
||||
if (!(key in results))
|
||||
results[key] = {
|
||||
email: item.primaryEmail,
|
||||
deleted: false,
|
||||
perms: [ ],
|
||||
groups: [ ],
|
||||
name: {
|
||||
first: item.name.givenName,
|
||||
last: item.name.familyName
|
||||
},
|
||||
};
|
||||
if (!(key in results))
|
||||
results[key] = {
|
||||
email: item.primaryEmail,
|
||||
deleted: false,
|
||||
perms: [],
|
||||
groups: [],
|
||||
name: {
|
||||
first: item.name.givenName,
|
||||
last: item.name.familyName
|
||||
},
|
||||
};
|
||||
|
||||
callback();
|
||||
},
|
||||
callback);
|
||||
};
|
||||
}
|
||||
callback();
|
||||
},
|
||||
callback);
|
||||
};
|
||||
}
|
||||
|
||||
function reduce_array(data, results) {
|
||||
return function(callback) {
|
||||
for (var item in data) {
|
||||
results.push(data[item]);
|
||||
}
|
||||
function reduce_array(data, results) {
|
||||
return function (callback) {
|
||||
for (var item in data) {
|
||||
results.push(data[item]);
|
||||
}
|
||||
|
||||
results.sort(function(a, b) {
|
||||
var result = a.name.first.toLowerCase().localeCompare(b.name.first.toLowerCase());
|
||||
if (result == 0)
|
||||
result = a.name.last.toLowerCase().localeCompare(b.name.last.toLowerCase());
|
||||
results.sort(function (a, b) {
|
||||
var result = a.name.first.toLowerCase().localeCompare(b.name.first.toLowerCase());
|
||||
if (result == 0)
|
||||
result = a.name.last.toLowerCase().localeCompare(b.name.last.toLowerCase());
|
||||
|
||||
return result;
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
callback();
|
||||
};
|
||||
}
|
||||
callback();
|
||||
};
|
||||
}
|
||||
|
||||
function merge_sources(data, callback) {
|
||||
var map = {};
|
||||
var reduce = [];
|
||||
function merge_sources(data, callback) {
|
||||
var map = {};
|
||||
var reduce = [];
|
||||
|
||||
async.series([
|
||||
map_local_users(data.local, map),
|
||||
map_gapps_users(data.gapps.users, map),
|
||||
reduce_array(map, reduce),
|
||||
],
|
||||
function(err) {
|
||||
callback(err, reduce);
|
||||
});
|
||||
}
|
||||
async.series([
|
||||
map_local_users(data.local, map),
|
||||
map_gapps_users(data.gapps.users, map),
|
||||
reduce_array(map, reduce),
|
||||
],
|
||||
function (err) {
|
||||
callback(err, reduce);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
index: function(req, res) {
|
||||
var criteria = { deleted: false };
|
||||
return {
|
||||
index: function (req, res) {
|
||||
var criteria = {deleted: false};
|
||||
|
||||
if (req.query.group) {
|
||||
criteria.groups = req.query.group;
|
||||
}
|
||||
if (req.query.group) {
|
||||
criteria.groups = req.query.group;
|
||||
}
|
||||
|
||||
if (req.query.perms) {
|
||||
criteria.perms = req.query.perms;
|
||||
}
|
||||
if (req.query.perms) {
|
||||
criteria.perms = req.query.perms;
|
||||
}
|
||||
|
||||
if (req.query.userid) {
|
||||
criteria._id = req.query.userid;
|
||||
}
|
||||
if (req.query.userid) {
|
||||
criteria._id = req.query.userid;
|
||||
}
|
||||
|
||||
var query = User.find(criteria)
|
||||
.select('name groups')
|
||||
.exec(function(err, results) {
|
||||
if (err) {
|
||||
res.json(500, err);
|
||||
} else {
|
||||
res.json(results);
|
||||
}
|
||||
});
|
||||
},
|
||||
var query = User.find(criteria)
|
||||
.select('name groups')
|
||||
.exec(function (err, results) {
|
||||
if (err) {
|
||||
res.json(500, err);
|
||||
} else {
|
||||
res.json(results);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
details: function(req, res) {
|
||||
details: function (req, res) {
|
||||
|
||||
async.waterfall([
|
||||
fetch_all_users,
|
||||
merge_sources,
|
||||
],
|
||||
function(err, results) {
|
||||
if (err) return res.json(500, err);
|
||||
res.json(results);
|
||||
});
|
||||
},
|
||||
async.waterfall([
|
||||
fetch_all_users,
|
||||
merge_sources,
|
||||
],
|
||||
function (err, results) {
|
||||
if (err) return res.json(500, err);
|
||||
res.json(results);
|
||||
});
|
||||
},
|
||||
|
||||
create: function(req, res) {
|
||||
log.info("users.create %j", req.body);
|
||||
create: function (req, res) {
|
||||
log.info("users.create %j", req.body);
|
||||
|
||||
var user = new User({
|
||||
email: req.body.email,
|
||||
name: req.body.name,
|
||||
groups: req.body.groups,
|
||||
perms: req.body.perms,
|
||||
deleted: false
|
||||
});
|
||||
var user = new User({
|
||||
email: req.body.email,
|
||||
name: req.body.name,
|
||||
groups: req.body.groups,
|
||||
perms: req.body.perms,
|
||||
deleted: false
|
||||
});
|
||||
|
||||
return user.save(function(err) {
|
||||
if (err)
|
||||
log.error("Error: %s", err);
|
||||
return user.save(function (err) {
|
||||
if (err)
|
||||
log.error("Error: %s", err);
|
||||
|
||||
return res.json(user);
|
||||
});
|
||||
},
|
||||
return res.json(user);
|
||||
});
|
||||
},
|
||||
|
||||
update: function(req, res) {
|
||||
var id = req.param('user_id');
|
||||
log.info("users.update %s %j", id, req.body);
|
||||
get: function(req, res, next) {
|
||||
var id = req.param('user_id');
|
||||
log.info("users.get %s", id);
|
||||
|
||||
return User.findById(id, function(err, user) {
|
||||
user.email = req.body.email;
|
||||
user.name = req.body.name;
|
||||
user.groups = req.body.groups;
|
||||
user.perms = req.body.perms;
|
||||
User.findById(id)
|
||||
.select('email picture perms groups name')
|
||||
.exec()
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
return next(new Error('Failed to load user ' + id));
|
||||
}
|
||||
res.json(user);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
|
||||
return user.save(function(err) {
|
||||
if (err)
|
||||
log.err("Error: %s", err);
|
||||
update: function (req, res) {
|
||||
var id = req.param('user_id');
|
||||
log.info("users.update %s %j", id, req.body);
|
||||
|
||||
return res.json(user);
|
||||
});
|
||||
});
|
||||
},
|
||||
return User.findById(id, function (err, user) {
|
||||
user.email = req.body.email;
|
||||
user.name = req.body.name;
|
||||
user.groups = req.body.groups;
|
||||
user.perms = req.body.perms;
|
||||
|
||||
clocks: function(req, res) {
|
||||
var id = req.param('user_id');
|
||||
return user.save(function (err) {
|
||||
if (err)
|
||||
log.err("Error: %s", err);
|
||||
|
||||
var criteria = {
|
||||
tech: id
|
||||
};
|
||||
return res.json(user);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
var query = Clock.find(criteria)
|
||||
.sort('-dt')
|
||||
.exec(function(err, results) {
|
||||
if (err) {
|
||||
res.json(500, err);
|
||||
} else {
|
||||
res.json(results);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
clocks: function (req, res) {
|
||||
var id = req.param('user_id');
|
||||
|
||||
var criteria = {
|
||||
tech: id
|
||||
};
|
||||
|
||||
var query = Clock.find(criteria)
|
||||
.sort('-dt')
|
||||
.exec(function (err, results) {
|
||||
if (err) {
|
||||
res.json(500, err);
|
||||
} else {
|
||||
res.json(results);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var blacklist = [
|
||||
"system@atlanticbiomedical.com",
|
||||
"admin@atlanticbiomedical.com",
|
||||
"amazons3@atlanticbiomedical.com",
|
||||
"api@atlanticbiomedical.com",
|
||||
"biodexservice@atlanticbiomedical.com",
|
||||
"cerberusapp@atlanticbiomedical.com",
|
||||
"chattservice@atlanticbiomedical.com",
|
||||
"dropbox@atlanticbiomedical.com",
|
||||
"inquiries@atlanticbiomedical.com",
|
||||
"office@atlanticbiomedical.com",
|
||||
"parts@atlanticbiomedical.com",
|
||||
"schedule@atlanticbiomedical.com",
|
||||
"webapp@atlanticbiomedical.com",
|
||||
"banfieldservice@atlanticbiomedical.com",
|
||||
"chris.sewell@atlanticbiomedical.com",
|
||||
"devel@atlanticbiomedical.com",
|
||||
"dobie@atlanticbiomedical.com",
|
||||
"system@atlanticbiomedical.com",
|
||||
"admin@atlanticbiomedical.com",
|
||||
"amazons3@atlanticbiomedical.com",
|
||||
"api@atlanticbiomedical.com",
|
||||
"biodexservice@atlanticbiomedical.com",
|
||||
"cerberusapp@atlanticbiomedical.com",
|
||||
"chattservice@atlanticbiomedical.com",
|
||||
"dropbox@atlanticbiomedical.com",
|
||||
"inquiries@atlanticbiomedical.com",
|
||||
"office@atlanticbiomedical.com",
|
||||
"parts@atlanticbiomedical.com",
|
||||
"schedule@atlanticbiomedical.com",
|
||||
"webapp@atlanticbiomedical.com",
|
||||
"banfieldservice@atlanticbiomedical.com",
|
||||
"chris.sewell@atlanticbiomedical.com",
|
||||
"devel@atlanticbiomedical.com",
|
||||
"dobie@atlanticbiomedical.com",
|
||||
// "akirayasha@gmail.com",
|
||||
"receipts@atlanticbiomedical.com",
|
||||
"receipts@atlanticbiomedical.com",
|
||||
];
|
||||
|
File diff suppressed because it is too large
Load Diff
30
app/model/timeClockException.js
Normal file
30
app/model/timeClockException.js
Normal file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
var mongoose = require('mongoose');
|
||||
var Schema = mongoose.Schema;
|
||||
var ObjectId = Schema.ObjectId;
|
||||
|
||||
const EXCEPTION_REASONS = [
|
||||
'late_to_first_workorder',
|
||||
'too_little_travel',
|
||||
'too_much_travel'
|
||||
];
|
||||
|
||||
var schema = new Schema({
|
||||
user: {
|
||||
type: ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
date: {
|
||||
type: Date,
|
||||
required: true
|
||||
},
|
||||
reason: {
|
||||
type: String,
|
||||
enum: EXCEPTION_REASONS,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('TimeClockException', schema);
|
@ -1,58 +1,106 @@
|
||||
"use strict";
|
||||
|
||||
var moment = require('moment');
|
||||
var mongoose = require('mongoose');
|
||||
var Schema = mongoose.Schema;
|
||||
var ObjectId = Schema.ObjectId;
|
||||
|
||||
var schema = new Schema({
|
||||
user: {
|
||||
type: ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
|
||||
user: {
|
||||
type: ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
|
||||
client: {
|
||||
type: ObjectId,
|
||||
ref: 'Client'
|
||||
},
|
||||
|
||||
workorder: {
|
||||
type: ObjectId,
|
||||
ref: 'Workorder'
|
||||
},
|
||||
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['open', 'closed']
|
||||
},
|
||||
|
||||
start: {
|
||||
type: Date
|
||||
},
|
||||
|
||||
end: {
|
||||
type: Date
|
||||
},
|
||||
|
||||
duration: {
|
||||
type: Number,
|
||||
min: 0
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
enum: [ 'workorder', 'workday', 'nonBillable' ]
|
||||
},
|
||||
|
||||
reason: {
|
||||
type: String,
|
||||
enum: ['shop', 'break', 'pto', 'meeting', 'event', 'weather', 'holiday']
|
||||
},
|
||||
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true
|
||||
client: {
|
||||
type: ObjectId,
|
||||
ref: 'Client',
|
||||
required: function() {
|
||||
return this.type === 'workorder';
|
||||
}
|
||||
},
|
||||
|
||||
workorder: {
|
||||
type: ObjectId,
|
||||
ref: 'Workorder',
|
||||
required: function() {
|
||||
return this.type === 'workorder';
|
||||
}
|
||||
},
|
||||
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['open', 'closed'],
|
||||
required: true
|
||||
},
|
||||
|
||||
start: {
|
||||
type: Date,
|
||||
required: true
|
||||
},
|
||||
|
||||
end: {
|
||||
type: Date,
|
||||
required: function() {
|
||||
return this.status === 'closed';
|
||||
}
|
||||
},
|
||||
|
||||
duration: {
|
||||
type: Number,
|
||||
min: 0
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['workorder', 'workday', 'nonBillable'],
|
||||
required: true
|
||||
},
|
||||
|
||||
reason: {
|
||||
type: String,
|
||||
enum: ['shop', 'break', 'pto', 'meeting', 'event', 'weather', 'holiday'],
|
||||
required: function() {
|
||||
return this.type === 'nonBillable';
|
||||
}
|
||||
},
|
||||
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true,
|
||||
required: function() {
|
||||
return this.type === 'workorder' && this.status === 'closed';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
schema.pre('save', function (next) {
|
||||
if (this.status === 'open') {
|
||||
this.end = undefined;
|
||||
}
|
||||
|
||||
if (this.type === 'workorder') {
|
||||
this.reason = undefined;
|
||||
}
|
||||
|
||||
if (this.type !== 'workorder') {
|
||||
this.workorder = undefined;
|
||||
this.client = undefined;
|
||||
}
|
||||
|
||||
if (this.start && this.end) {
|
||||
this.duration = moment(this.end).diff(this.start, 'seconds');
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
schema.pre('validate', function (next) {
|
||||
if (this.start > this.end) {
|
||||
return next(new Error('End date must be greater than start date.'));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('TimeClockSpan', schema);
|
||||
|
@ -11,10 +11,26 @@ var userSchema = new Schema({
|
||||
picture: String,
|
||||
refreshToken: String,
|
||||
accessToken: String,
|
||||
|
||||
groups: [String],
|
||||
perms: [String],
|
||||
deleted: { type: Boolean, default: false }
|
||||
|
||||
employmentType: {
|
||||
type: String,
|
||||
enum: ['hourly', 'salary'],
|
||||
default: 'hourly',
|
||||
required: true
|
||||
},
|
||||
|
||||
unpaidTravel: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true
|
||||
},
|
||||
|
||||
deleted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
userSchema.methods.hasPermission = function(perm) {
|
||||
|
@ -1,37 +1,70 @@
|
||||
var mongoose = require('mongoose')
|
||||
Schema = mongoose.Schema,
|
||||
ObjectId = Schema.ObjectId;
|
||||
Schema = mongoose.Schema,
|
||||
ObjectId = Schema.ObjectId;
|
||||
|
||||
var workorderSchema = new Schema({
|
||||
biomedId: Number,
|
||||
client: { type: ObjectId, ref: 'Client' },
|
||||
emails: [String],
|
||||
createdOn: Date,
|
||||
createdBy: { type: ObjectId, ref: 'User' },
|
||||
modifiedBy: { type: ObjectId, ref: 'User' },
|
||||
reason: String,
|
||||
maintenanceType: String,
|
||||
remarks: String,
|
||||
status: String,
|
||||
scheduling: {
|
||||
start: Date,
|
||||
end: Date
|
||||
},
|
||||
calendarId: String,
|
||||
techs: [{ type: ObjectId, ref: 'User' }],
|
||||
history: [{
|
||||
oldValues: {},
|
||||
newValues: {},
|
||||
modifiedBy: { type: ObjectId, ref: 'User' }
|
||||
}],
|
||||
deleted: { type: Boolean, default: false },
|
||||
invoiceNumber: String,
|
||||
invoicedOn: Date,
|
||||
checkNumber: String,
|
||||
paidOn: Date,
|
||||
alternativeContact: String,
|
||||
trackingNumber: String,
|
||||
devices: [{ type: ObjectId, ref: 'Device' }]
|
||||
biomedId: Number,
|
||||
client: {type: ObjectId, ref: 'Client'},
|
||||
emails: [String],
|
||||
createdOn: Date,
|
||||
createdBy: {type: ObjectId, ref: 'User'},
|
||||
modifiedBy: {type: ObjectId, ref: 'User'},
|
||||
reason: String,
|
||||
maintenanceType: String,
|
||||
remarks: String,
|
||||
status: String,
|
||||
scheduling: {
|
||||
start: Date,
|
||||
end: Date
|
||||
},
|
||||
calendarId: String,
|
||||
techs: [{type: ObjectId, ref: 'User'}],
|
||||
history: [{
|
||||
oldValues: {},
|
||||
newValues: {},
|
||||
modifiedBy: {type: ObjectId, ref: 'User'}
|
||||
}],
|
||||
deleted: {type: Boolean, default: false},
|
||||
invoiceNumber: String,
|
||||
invoicedTime: String,
|
||||
invoicedOn: Date,
|
||||
checkNumber: String,
|
||||
paidOn: Date,
|
||||
alternativeContact: String,
|
||||
trackingNumber: String,
|
||||
devices: [{type: ObjectId, ref: 'Device'}],
|
||||
workorderType: {
|
||||
type: String,
|
||||
enum: [
|
||||
'office',
|
||||
'anesthesia',
|
||||
'biomed',
|
||||
'bsp',
|
||||
'ice',
|
||||
'imaging',
|
||||
'sales',
|
||||
'sterile-processing',
|
||||
'depot',
|
||||
'trace-gas',
|
||||
'room-air-exchange',
|
||||
'isolation-panels',
|
||||
'ups-systems',
|
||||
'relocation',
|
||||
'ice-maker',
|
||||
'waste-management-system',
|
||||
'medgas',
|
||||
'staffing',
|
||||
'ert',
|
||||
'shop',
|
||||
'break',
|
||||
'pto',
|
||||
'meeting',
|
||||
'event',
|
||||
'weather',
|
||||
'legacy'
|
||||
],
|
||||
default: 'legacy'
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Workorder', workorderSchema);
|
||||
|
60
app/routes/exceptions.js
Normal file
60
app/routes/exceptions.js
Normal file
@ -0,0 +1,60 @@
|
||||
"use strict";
|
||||
|
||||
const moment = require('moment-timezone');
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* GET /api/exceptions
|
||||
*/
|
||||
function index(req, res) {
|
||||
req.check('user').isMongoId();
|
||||
req.check('date').optional().isISO8601();
|
||||
req.check('week').optional().isISO8601();
|
||||
|
||||
if (!req.query.date && !req.query.week) {
|
||||
return res.json({
|
||||
error: {
|
||||
message: 'Must specify a date filter.'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
user: req.query.user
|
||||
};
|
||||
|
||||
if (req.query.date) {
|
||||
const date = moment(req.query.date);
|
||||
|
||||
const startOfDay = date.clone().startOf('day');
|
||||
const endOfDay = date.clone().endOf('day');
|
||||
|
||||
query.date = {
|
||||
$gte: startOfDay,
|
||||
$lte: endOfDay
|
||||
};
|
||||
}
|
||||
|
||||
if (req.query.week) {
|
||||
const week = moment(req.query.week);
|
||||
|
||||
const startOfWeek = week.clone().startOf('week');
|
||||
const endOfWeek = week.clone().endOf('week');
|
||||
|
||||
query.date = {
|
||||
$gte: startOfWeek,
|
||||
$lte: endOfWeek
|
||||
};
|
||||
}
|
||||
|
||||
var results = req.db.TimeClockException
|
||||
.find(query)
|
||||
.sort('date')
|
||||
.exec();
|
||||
|
||||
res.promise(results);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
index
|
||||
};
|
9
app/routes/index.js
Normal file
9
app/routes/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
exceptions: require('./exceptions'),
|
||||
misc: require('./misc'),
|
||||
spans: require('./spans'),
|
||||
users: require('./users'),
|
||||
workorders: require('./workorders')
|
||||
};
|
53
app/routes/misc.js
Normal file
53
app/routes/misc.js
Normal file
@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
const email = require('../util/email');
|
||||
const moment = require('moment-timezone');
|
||||
const _ = require('lodash');
|
||||
var config = require('../../config/config')['prod'];
|
||||
|
||||
var partsRequestTemplate = email.template(
|
||||
'partsRequest.html.tmpl',
|
||||
'partsRequest.text.tmpl',
|
||||
'Parts Request',
|
||||
[
|
||||
'techName',
|
||||
'requestDate',
|
||||
'customerId',
|
||||
'customerName',
|
||||
'customerContact',
|
||||
'customerPhone',
|
||||
'biomedId',
|
||||
'manufacture',
|
||||
'model',
|
||||
'serialNumber',
|
||||
'vendor',
|
||||
'vendorPhone',
|
||||
'partNumber',
|
||||
'description',
|
||||
'specialNotes'
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* POST /api/misc/partsRequest
|
||||
*/
|
||||
function partsRequest(req, res) {
|
||||
const message = {
|
||||
to: config.email.partsRequest
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
techName: `${req.user.name.first} ${req.user.name.last}`,
|
||||
requestDate: moment().format('LLLL')
|
||||
};
|
||||
|
||||
const values = _.assign({}, req.body, defaultValues);
|
||||
|
||||
res.promise(email.send(message, partsRequestTemplate, values));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
partsRequest
|
||||
};
|
97
app/routes/spans.js
Normal file
97
app/routes/spans.js
Normal file
@ -0,0 +1,97 @@
|
||||
"use strict";
|
||||
|
||||
const moment = require('moment-timezone');
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* GET /api/spans
|
||||
*/
|
||||
function index(req, res) {
|
||||
req.check('user').optional().isMongoId();
|
||||
req.check('workorder').optional().isMongoId();
|
||||
req.check('date').optional().isISO8601();
|
||||
req.check('week').optional().isISO8601();
|
||||
|
||||
if (!req.query.user && !req.query.workorder) {
|
||||
return res.json({
|
||||
error: {
|
||||
message: 'Must specify a type filter.'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (req.query.user && !req.query.date && !req.query.week) {
|
||||
return res.json({
|
||||
error: {
|
||||
message: 'Must specify a date filter.'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const query = {};
|
||||
|
||||
if (req.query.user) {
|
||||
query.user = req.query.user;
|
||||
}
|
||||
|
||||
if (req.query.workorder) {
|
||||
query.workorder = req.query.workorder;
|
||||
}
|
||||
|
||||
if (req.query.date) {
|
||||
const date = moment(req.query.date);
|
||||
|
||||
const startOfDay = date.clone().startOf('day');
|
||||
const endOfDay = date.clone().endOf('day');
|
||||
|
||||
query.start = {
|
||||
$gte: startOfDay,
|
||||
$lte: endOfDay
|
||||
};
|
||||
}
|
||||
|
||||
if (req.query.week) {
|
||||
const week = moment(req.query.week);
|
||||
|
||||
const startOfWeek = week.clone().startOf('week');
|
||||
const endOfWeek = week.clone().endOf('week');
|
||||
|
||||
query.start = {
|
||||
$gte: startOfWeek,
|
||||
$lte: endOfWeek
|
||||
};
|
||||
}
|
||||
|
||||
var results = req.db.TimeClockSpan
|
||||
.find(query)
|
||||
.sort('start')
|
||||
.exec();
|
||||
|
||||
res.promise(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/spans/:user_id
|
||||
*/
|
||||
function update(req, res) {
|
||||
req.check('id').isMongoId();
|
||||
|
||||
var data = req.body;
|
||||
delete data._id;
|
||||
delete data.__v;
|
||||
|
||||
const result = req.db.TimeClockSpan
|
||||
.findById(req.params.id)
|
||||
.exec()
|
||||
.then((entity) => {
|
||||
_.assign(entity, data);
|
||||
return entity.save();
|
||||
});
|
||||
|
||||
res.promise(result);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
index,
|
||||
update
|
||||
};
|
184
app/routes/users.js
Normal file
184
app/routes/users.js
Normal file
@ -0,0 +1,184 @@
|
||||
"use strict";
|
||||
|
||||
const Promise = require('bluebird');
|
||||
const moment = require('moment-timezone');
|
||||
const _ = require('lodash');
|
||||
|
||||
function filterFields(user) {
|
||||
const obj = user.toObject();
|
||||
delete obj.accessToken;
|
||||
delete obj.refreshToken;
|
||||
return obj
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/users/:id
|
||||
*/
|
||||
function fetch(req, res) {
|
||||
let result;
|
||||
|
||||
if (req.params.id === 'me') {
|
||||
result = Promise.resolve(req.user);
|
||||
} else {
|
||||
result = req.db.User.findById(req.params.id);
|
||||
}
|
||||
|
||||
res.promise(result.then(filterFields));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/users/:id
|
||||
*/
|
||||
function update(req, res) {
|
||||
req.check('id').isMongoId();
|
||||
|
||||
var data = req.body;
|
||||
delete data._id;
|
||||
delete data.__v;
|
||||
delete data.name;
|
||||
delete data.email;
|
||||
delete data.accessToken;
|
||||
delete data.refreshToken;
|
||||
delete data.deleted;
|
||||
|
||||
const result = req.db.User
|
||||
.findById(req.params.id)
|
||||
.exec()
|
||||
.then((entity) => {
|
||||
_.assign(entity, data);
|
||||
return entity.save();
|
||||
})
|
||||
.then(filterFields);
|
||||
|
||||
res.promise(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/users/:id/daysWorked
|
||||
*/
|
||||
function daysWorked(req, res) {
|
||||
const id = req.params.id === 'me' ? req.user.id : req.params.id;
|
||||
|
||||
const query = {
|
||||
user: id,
|
||||
type: 'workday'
|
||||
};
|
||||
|
||||
const result = req.db.TimeClockSpan
|
||||
.find(query)
|
||||
.exec()
|
||||
.then((records) => _.chain(records).reduce(accumulateDaysWorked, []).values());
|
||||
|
||||
res.promise(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/users/:id/weeksWorked
|
||||
*/
|
||||
function weeksWorked(req, res) {
|
||||
const id = req.params.id === 'me' ? req.user.id : req.params.id;
|
||||
|
||||
const query = {
|
||||
user: id,
|
||||
type: 'workday'
|
||||
};
|
||||
|
||||
const result = req.db.TimeClockSpan
|
||||
.find(query)
|
||||
.exec()
|
||||
.then((records) => _.chain(records).reduce(accumulateWeeksWorked, []).values());
|
||||
|
||||
res.promise(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/users/:id/timeClock
|
||||
*/
|
||||
function timeClock(req, res) {
|
||||
const id = req.params.id === 'me' ? req.user.id : req.params.id;
|
||||
const date = moment(req.query.date);
|
||||
const startOfDay = date.clone().startOf('day');
|
||||
const endOfDay = date.clone().endOf('day');
|
||||
|
||||
var spans = req.db.TimeClockSpan
|
||||
.find({
|
||||
user: id,
|
||||
start: {
|
||||
$gte: startOfDay,
|
||||
$lte: endOfDay
|
||||
}
|
||||
})
|
||||
.sort({ start: 1 })
|
||||
.exec();
|
||||
|
||||
var exceptions = req.db.TimeClockException
|
||||
.find({
|
||||
user: id,
|
||||
date: {
|
||||
$gte: startOfDay,
|
||||
$lte: endOfDay
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
|
||||
var workorders = spans
|
||||
.then(extractIds('workorder'))
|
||||
.then((ids) => {
|
||||
return req.db.Workorder
|
||||
.find({
|
||||
_id: {
|
||||
$in: ids
|
||||
}
|
||||
})
|
||||
.populate('client', 'name identifier')
|
||||
.exec();
|
||||
})
|
||||
.then(indexById);
|
||||
|
||||
res.promise(Promise.props({
|
||||
spans,
|
||||
exceptions,
|
||||
workorders
|
||||
}));
|
||||
}
|
||||
|
||||
function accumulateDaysWorked(result, record) {
|
||||
const date = moment(record.start).local().startOf('day').format('YYYY-MM-DD');
|
||||
|
||||
if (result.indexOf(date) === -1) {
|
||||
result.push(date);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function accumulateWeeksWorked(result, record) {
|
||||
const date = moment(record.start).local().startOf('week').format('YYYY-MM-DD');
|
||||
|
||||
if (result.indexOf(date) === -1) {
|
||||
result.push(date);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractIds(field) {
|
||||
return (data) => _(data)
|
||||
.pluck(field)
|
||||
.reject(_.isUndefined)
|
||||
.uniq((id) => id.toString())
|
||||
.value();
|
||||
}
|
||||
|
||||
function indexById(data) {
|
||||
return _.indexBy(data, 'id')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetch,
|
||||
update,
|
||||
daysWorked,
|
||||
weeksWorked,
|
||||
timeClock
|
||||
};
|
67
app/routes/workorders.js
Normal file
67
app/routes/workorders.js
Normal file
@ -0,0 +1,67 @@
|
||||
"use strict";
|
||||
|
||||
const Promise = require('bluebird');
|
||||
const moment = require('moment-timezone');
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* GET /api/workorders/:id/timeClock
|
||||
*/
|
||||
function timeClock(req, res) {
|
||||
req.check('id').isMongoId();
|
||||
|
||||
const id = req.params.id;
|
||||
|
||||
const spans = req.db.TimeClockSpan
|
||||
.find({
|
||||
workorder: id
|
||||
})
|
||||
.exec();
|
||||
|
||||
const users = spans
|
||||
.then(extractIds('user'))
|
||||
.then((ids) => {
|
||||
return req.db.User
|
||||
.find({
|
||||
_id: {
|
||||
$in: ids
|
||||
}
|
||||
})
|
||||
.select('name picture')
|
||||
.exec();
|
||||
})
|
||||
.then(indexById);
|
||||
|
||||
const duration = spans
|
||||
.then((spans) => {
|
||||
let result = 0;
|
||||
spans.forEach((span) => {
|
||||
if (span.duration) {
|
||||
result += span.duration;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
res.promise(Promise.props({
|
||||
duration,
|
||||
spans,
|
||||
users
|
||||
}));
|
||||
}
|
||||
|
||||
function extractIds(field) {
|
||||
return (data) => _(data)
|
||||
.pluck(field)
|
||||
.reject(_.isUndefined)
|
||||
.uniq((id) => id.toString())
|
||||
.value();
|
||||
}
|
||||
|
||||
function indexById(data) {
|
||||
return _.indexBy(data, 'id')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
timeClock
|
||||
};
|
74
app/templates/exception.html.tmpl
Normal file
74
app/templates/exception.html.tmpl
Normal file
@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
</head>
|
||||
<body style="background-color:#eee;color:#333;font-family:Helvetica, Arial, sans-serif;line-height:1.25;-webkit-text-size-adjust:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="20" cellspacing="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#fff">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="48" width="100%" style="background-color:#eee;">
|
||||
<tr>
|
||||
<td valign="middle">
|
||||
<h3 style="font-weight: 300">Atlantic Biomedical</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="48" width="100%" style="background-color:#fff;">
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<h4 style="font-weight: 300">Tech Exception</h4>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="1" width="100%">
|
||||
<tr>
|
||||
<td align="center" valign="middle" style="background-color: #eeeeee" width="199"></td>
|
||||
<td align="center" valign="middle" style="background-color: #039BE5" width="202"></td>
|
||||
<td align="center" valign="middle" style="background-color: #eeeeee" width="199"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding:24px;">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td colspan="2">The following exception was made by <b><%- techName %></b> at <b><%- date %></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><%- message %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
3
app/templates/exception.text.tmpl
Normal file
3
app/templates/exception.text.tmpl
Normal file
@ -0,0 +1,3 @@
|
||||
The following exception was made by <%- techName %> on <%- date %>
|
||||
|
||||
<%- message %>
|
146
app/templates/partsRequest.html.tmpl
Normal file
146
app/templates/partsRequest.html.tmpl
Normal file
@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
</head>
|
||||
<body style="background-color:#eee;color:#333;font-family:Helvetica, Arial, sans-serif;line-height:1.25;-webkit-text-size-adjust:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="20" cellspacing="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#fff">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="48" width="100%" style="background-color:#eee;">
|
||||
<tr>
|
||||
<td valign="middle">
|
||||
<h3 style="font-weight: 300">Atlantic Biomedical</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="48" width="100%" style="background-color:#fff;">
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<h4 style="font-weight: 300">Parts Request</h4>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="1" width="100%">
|
||||
<tr>
|
||||
<td align="center" valign="middle" style="background-color: #eeeeee" width="199"></td>
|
||||
<td align="center" valign="middle" style="background-color: #039BE5" width="202"></td>
|
||||
<td align="center" valign="middle" style="background-color: #eeeeee" width="199"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding:24px;">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td colspan="2">The following request for parts was made by <b><%- techName %></b> on <b><%- requestDate %></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><h4>Customer</h4></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td><%- customerId %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td><%- customerName %></td>
|
||||
</tr>
|
||||
<% if (customerContact) { %>
|
||||
<tr>
|
||||
<td>Contact</td>
|
||||
<td><%- customerContact %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% if (customerPhone) { %>
|
||||
<tr>
|
||||
<td>Phone Number</td>
|
||||
<td><%- customerPhone %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% if (biomedId) { %>
|
||||
<tr>
|
||||
<td>Atlantic Biomedical ID</td>
|
||||
<td><%- biomedId %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<tr style="margin-top: 24px;">
|
||||
<td colspan="2"><h4>Part Details</h4></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manufacture</td>
|
||||
<td><%- manufacture %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td><%- model %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial Number</td>
|
||||
<td><%- serialNumber %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vendor</td>
|
||||
<td><%- vendor %></td>
|
||||
</tr>
|
||||
<% if (vendorPhone) { %>
|
||||
<tr>
|
||||
<td>Vendor Phone Number</td>
|
||||
<td><%- vendorPhone %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<tr>
|
||||
<td>Part Number</td>
|
||||
<td><%- partNumber %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><h4>Other Details</h4></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">Brief Description</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><%- description %></td>
|
||||
</tr>
|
||||
<% if (specialNotes) { %>
|
||||
<tr>
|
||||
<td colspan="2" style="padding-top: 16px">Special Notes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><%- specialNotes %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
58
app/templates/partsRequest.text.tmpl
Normal file
58
app/templates/partsRequest.text.tmpl
Normal file
@ -0,0 +1,58 @@
|
||||
The following request for parts was made by <%- techName %> on <%- requestDate %>
|
||||
|
||||
Customer
|
||||
ID:
|
||||
<%- customerId %>
|
||||
|
||||
Name:
|
||||
<%- customerName %>
|
||||
|
||||
<% if (customerContact) { %>
|
||||
Contact:
|
||||
<%- customerContact %>
|
||||
|
||||
<% } %>
|
||||
<% if (customerPhone) { %>
|
||||
Phone Number:
|
||||
<%- customerPhone %>
|
||||
|
||||
<% } %>
|
||||
<% if (biomedId) { %>
|
||||
Atlantic Biomedical ID:
|
||||
<%- biomedId %>
|
||||
|
||||
<% } %>
|
||||
|
||||
|
||||
Part Details
|
||||
Manufacture:
|
||||
<%- manufacture %>
|
||||
|
||||
Model:
|
||||
<%- model %>
|
||||
|
||||
|
||||
Serial Number:
|
||||
<%- serialNumber %>
|
||||
|
||||
Vendor:
|
||||
<%- vendor %>
|
||||
|
||||
<% if (vendorPhone) { %>
|
||||
Vendor Phone Number:
|
||||
<%- vendorPhone %>
|
||||
|
||||
<% } %>
|
||||
|
||||
Part Number:
|
||||
<%- partNumber %>
|
||||
|
||||
|
||||
Other Details
|
||||
Brief Description:
|
||||
<%- description %>
|
||||
<% if (specialNotes) { %>
|
||||
|
||||
Special Notes:
|
||||
<%- specialNotes %>
|
||||
<% } %>
|
100
app/util/email.js
Normal file
100
app/util/email.js
Normal file
@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
const email = require('emailjs');
|
||||
const _ = require('lodash');
|
||||
const path = require('path');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const fs = Promise.promisifyAll(require('fs'));
|
||||
var config = require('../../config/config')['prod'];
|
||||
|
||||
const DEFAULT_HEADERS = {
|
||||
from: 'api@atlanticbiomedical.com'
|
||||
};
|
||||
|
||||
function template(htmlFile, textFile, subjectTemplate, defaultValues) {
|
||||
if (_.isArray(defaultValues)) {
|
||||
defaultValues = _.reduce(defaultValues, (result, key) => {
|
||||
result[key] = '';
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const builder = template => {
|
||||
template = _.template(template);
|
||||
return (values) => {
|
||||
values = _.assign({}, defaultValues, values);
|
||||
return template(values);
|
||||
};
|
||||
};
|
||||
|
||||
var htmlTemplate = fs.readFileAsync(path.join(__dirname, '../templates', htmlFile));
|
||||
var textTemplate = fs.readFileAsync(path.join(__dirname, '../templates', textFile));
|
||||
|
||||
return Promise
|
||||
.props({
|
||||
html: htmlTemplate,
|
||||
text: textTemplate,
|
||||
subject: subjectTemplate
|
||||
})
|
||||
.then(templates => {
|
||||
return {
|
||||
html: builder(templates.html),
|
||||
text: builder(templates.text),
|
||||
subject: builder(templates.subject)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function send(headers, template, values) {
|
||||
var message = _.assign({}, DEFAULT_HEADERS, headers);
|
||||
message = prepairHeaders(message);
|
||||
|
||||
return template.then(tmpl => {
|
||||
message.text = tmpl.text(values);
|
||||
message.subject = tmpl.subject(values);
|
||||
message.attachment = [
|
||||
{data: tmpl.html(values), alternative: true}
|
||||
];
|
||||
|
||||
const server = email.server.connect({
|
||||
user: config.email.user,
|
||||
password: config.email.password,
|
||||
host: 'smtp.gmail.com',
|
||||
ssl: true
|
||||
});
|
||||
|
||||
return Promise.fromCallback(callback => {
|
||||
server.send(message, callback);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function prepairHeaders(headers) {
|
||||
return _.reduce(headers, (result, header, key) => {
|
||||
if (!_.isArray(header)) {
|
||||
header = [header];
|
||||
}
|
||||
|
||||
if (key === 'subject') {
|
||||
result[key] = header;
|
||||
} else {
|
||||
result[key] = _
|
||||
.map(header, entry => {
|
||||
if (_.isPlainObject(entry)) {
|
||||
return `${entry.name} <${entry.email}>`;
|
||||
} else {
|
||||
return `<${entry}>`;
|
||||
}
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
send,
|
||||
template
|
||||
};
|
Reference in New Issue
Block a user