Files
biomedjs/app/controllers/timesheet.js
Dobie Wollert a83a046933 fixed a bug
2015-12-21 00:47:26 -08:00

431 lines
9.6 KiB
JavaScript

"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 TimeSheet = mongoose.model('TimeSheet');
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);
spans.then(console.log);
return buildReport(spans, params.week);
}
function userSummaryHandler(params) {
const spans = findUserSpansForWeek(params.id, params.week);
return buildReport(spans, params.week);
}
function buildReport(spans, week) {
const workordersById = spans
.then(extractIds('workorder'))
.then(findWorkordersById)
.then(indexById);
const userIds = spans
.then(extractIds('user'));
const usersById = userIds
.then(findUsersById)
.then(indexById);
const timesheetsByUser = userIds
.then(findTimesheetsByUser(week))
.then(indexByUser);
return Promise.join(spans, workordersById, usersById, timesheetsByUser, generateSummary);
}
function generateSummary(spans, workordersById, usersById, timesheetsByUser) {
var results = {};
function fetchOrCreateUserRecord(userId) {
var record = results[userId];
if (!record) {
var user = usersById[userId];
var timesheet = timesheetsByUser[userId];
record = results[userId] = {
user: {
_id: user._id,
name: user.name
},
hasOpenSpans: false,
approved: !!(timesheet && timesheet.approved),
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.type === 'workorder') {
var workorder = workordersById[span.workorder];
addWorkorder(user, workorder);
}
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 {
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 indexByUser(data) {
return _.indexBy(data, 'user')
}
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 findTimesheetsByUser(week) {
return ids => {
const query = {
user: {
$in: ids
},
week: week
};
return TimeSheet.find(query).exec();
}
}
function findAllSpansForWeek(week) {
console.log(week.format());
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();
}
function approvalHandler(req, res) {
return (params) => {
params.week = params.week.toDate();
var query = {
user: params.id,
week: params.week
};
req.db.TimeSheet.findOne(query)
.then(timesheet => {
if (!timesheet) {
timesheet = new req.db.TimeSheet({
user: params.id,
week: params.week
});
}
timesheet.approved = true;
timesheet.approvedOn = new Date();
res.promise(timesheet.save());
});
};
}
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) {
Promise
.props({
week: validateWeek(req)
})
.then(summaryHandler)
.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));
},
approve: function(req, res) {
Promise
.props({
id: validateUserId(req),
week: validateWeek(req)
})
.then(approvalHandler(req, res))
.catch(errorHandler(res));
}
}
};