This commit is contained in:
Dobie Wollert
2015-12-16 09:12:35 -08:00
parent f9c9672818
commit f94ca33b9e
805 changed files with 67409 additions and 24609 deletions

View File

@ -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
View 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();
}

View File

@ -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) {

View File

@ -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));
},
}
};

View 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));
}
}
};

View File

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