mirror of
https://github.com/atlanticbiomedical/biomedjs.git
synced 2025-07-02 00:47:26 -04:00
774 lines
19 KiB
JavaScript
774 lines
19 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 TimeClockException = mongoose.model('TimeClockException');
|
|
var TimeSheet = mongoose.model('TimeSheet');
|
|
var Workorder = mongoose.model('Workorder');
|
|
var Device = mongoose.model('Device');
|
|
var DeviceType = mongoose.model('DeviceType');
|
|
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'];
|
|
|
|
var exceptionTemplate = email.template(
|
|
'exception.html.tmpl',
|
|
'exception.text.tmpl',
|
|
'Exception',
|
|
[
|
|
'techName',
|
|
'date',
|
|
'message'
|
|
]
|
|
);
|
|
|
|
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 hasTechApprovedPreviousWeek(user, day) {
|
|
var startOfWeek = day.clone().startOf('week').subtract(1, 'week').toDate();
|
|
var endOfWeek = day.clone().endOf('week').subtract(1, 'week').toDate();
|
|
|
|
return Promise
|
|
.props({
|
|
spans: TimeClockSpan.count({
|
|
start: {
|
|
'$gte': startOfWeek,
|
|
'$lte': endOfWeek
|
|
},
|
|
user: user
|
|
}),
|
|
timesheet: TimeSheet.findOne({
|
|
week: startOfWeek,
|
|
user: user
|
|
})
|
|
})
|
|
.then((props) => {
|
|
console.log('Spans last week: ', props.spans);
|
|
console.log('Previous timesheet', props.timesheet);
|
|
|
|
var approved = props.spans == 0 || (props.timesheet && props.timesheet.approved);
|
|
console.log('Status: ', approved);
|
|
|
|
return approved;
|
|
});
|
|
}
|
|
|
|
function findUserSpans(user, day) {
|
|
var startOfDay = day.clone().startOf('day').toDate();
|
|
var endOfDay = day.clone().endOf('day').toDate();
|
|
|
|
var query = {
|
|
start: {
|
|
'$gte': startOfDay,
|
|
'$lte': endOfDay
|
|
},
|
|
user: user
|
|
};
|
|
|
|
return TimeClockSpan
|
|
.find(query)
|
|
.exec();
|
|
}
|
|
|
|
function findUserSpansForWeek(user, day) {
|
|
var startOfDay = day.clone().startOf('week').toDate();
|
|
var endOfDay = day.clone().endOf('week').toDate();
|
|
|
|
var query = {
|
|
start: {
|
|
'$gte': startOfDay,
|
|
'$lte': endOfDay
|
|
},
|
|
user: user
|
|
};
|
|
|
|
return TimeClockSpan
|
|
.find(query)
|
|
.exec();
|
|
}
|
|
|
|
function findUserWorkorders(user, day) {
|
|
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
|
|
}
|
|
};
|
|
|
|
return Workorder
|
|
.find(query)
|
|
.populate('client', 'name identifier')
|
|
.exec();
|
|
}
|
|
|
|
function findUserWorkorder(id, user) {
|
|
var query = {
|
|
_id: id,
|
|
techs: user.id,
|
|
deleted: false
|
|
};
|
|
|
|
return Workorder
|
|
.findOne({
|
|
_id: id,
|
|
techs: user.id,
|
|
deleted: false
|
|
})
|
|
.populate('client', 'name identifier contacts address')
|
|
.then(workorder => {
|
|
var ids = _(workorder.devices)
|
|
.reject(_.isUndefined)
|
|
.uniq(id => id.toString())
|
|
.value();
|
|
|
|
return Device
|
|
.find({
|
|
_id: {
|
|
$in: ids
|
|
}
|
|
})
|
|
.populate('deviceType')
|
|
.select('biomedId deviceType serialNumber')
|
|
.then(devices => {
|
|
workorder.devices = devices;
|
|
return workorder;
|
|
});
|
|
})
|
|
}
|
|
|
|
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
|
|
};
|
|
|
|
return _.chain(spans)
|
|
.filter(function (span) {
|
|
|
|
if (filter.type && filter.type.indexOf(span.type) === -1) {
|
|
return false;
|
|
}
|
|
|
|
if (filter.status && String(span.status) !== filter.status) {
|
|
return false;
|
|
}
|
|
|
|
if (filter.workorder && String(span.workorder) !== filter.workorder) {
|
|
return false;
|
|
}
|
|
|
|
if (filter.reason && span.reason !== filter.reason) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.sortBy('start')
|
|
.value();
|
|
}
|
|
|
|
function spansStatus(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';
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function validateClockRequest(req) {
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
var params = {};
|
|
|
|
var type = req.body.type;
|
|
if (!type) {
|
|
return reject("Missing required parameter 'type'");
|
|
}
|
|
|
|
if (TASK_TYPES.indexOf(type) === -1) {
|
|
return reject("Invalid type: '" + type + "'");
|
|
}
|
|
|
|
params.type = type;
|
|
|
|
if (type === 'workorder') {
|
|
var id = req.body.id;
|
|
if (!id) {
|
|
return reject("Missing required parameter 'id'");
|
|
}
|
|
|
|
params.id = id;
|
|
}
|
|
|
|
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 + "'");
|
|
}
|
|
|
|
params.reason = reason;
|
|
}
|
|
|
|
resolve(params);
|
|
});
|
|
}
|
|
|
|
function validateWorkorderDetailsRequest(req) {
|
|
var params = {};
|
|
|
|
var id = req.param('id');
|
|
if (!id) {
|
|
return Promise.reject("Missing required parameter 'id'");
|
|
}
|
|
|
|
params.id = id;
|
|
|
|
return Promise.resolve(params);
|
|
}
|
|
|
|
function handleStatusRequest(spans, workorders) {
|
|
|
|
var results = {};
|
|
|
|
var workdaySpans = _.filter(spans, {type: 'workday'});
|
|
|
|
results.tasks = [];
|
|
|
|
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});
|
|
|
|
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});
|
|
|
|
return {
|
|
type: 'nonBillable',
|
|
reason: nonBillable,
|
|
status: spansStatus(nonBillableSpans),
|
|
spans: _.map(nonBillableSpans, spanToResponse)
|
|
};
|
|
})
|
|
.value()
|
|
);
|
|
|
|
return results;
|
|
}
|
|
|
|
function handleClockInRequest(params, user, spans, workorders, now) {
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
return span.save().then(spanToResponse);
|
|
}
|
|
|
|
function handleClockInExceptions(user, workorder, spans, now) {
|
|
|
|
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 (minutes > 15) {
|
|
|
|
new TimeClockException({
|
|
user: user._id,
|
|
date: new Date(),
|
|
reason: 'late_to_first_workorder'
|
|
}).save();
|
|
|
|
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 handleClockOutExceptions(currentSpan, user, spans, now) {
|
|
if (currentSpan.type !== 'workday') {
|
|
return;
|
|
}
|
|
|
|
var dayOfWeek = now.day();
|
|
if (dayOfWeek != 3 && dayOfWeek != 4) {
|
|
return;
|
|
}
|
|
|
|
var workdaySpans = filterSpans(spans, {type: 'workday'});
|
|
|
|
var secondsWorked = 0;
|
|
workdaySpans.forEach(span => secondsWorked += span.status === 'closed' ? span.duration : 0);
|
|
secondsWorked += currentSpan.duration;
|
|
|
|
console.log("Seconds Worked: " + secondsWorked);
|
|
|
|
if (dayOfWeek == 3 && secondsWorked < 20 * 60 * 60) {
|
|
new TimeClockException({
|
|
user: user._id,
|
|
date: new Date(),
|
|
reason: 'less_than_twenty_hours_worked'
|
|
}).save();
|
|
|
|
reportException({
|
|
user: user,
|
|
reason: 'Less than 20 hours worked by end of day Wednesday.'
|
|
});
|
|
}
|
|
|
|
if (dayOfWeek == 4 && secondsWorked < 30 * 60 * 60) {
|
|
new TimeClockException({
|
|
user: user._id,
|
|
date: new Date(),
|
|
reason: 'less_than_thirty_hours_worked'
|
|
}).save();
|
|
|
|
reportException({
|
|
user: user,
|
|
reason: 'Less than 30 hours worked by end of day Thursday.'
|
|
});
|
|
}
|
|
|
|
if (dayOfWeek == 4 && secondsWorked > 40 * 60 * 60) {
|
|
new TimeClockException({
|
|
user: user._id,
|
|
date: new Date(),
|
|
reason: 'greater_than_forty_hours_worked'
|
|
}).save();
|
|
|
|
reportException({
|
|
user: user,
|
|
reason: 'Greater than 40 hours worked by end of day Thursday.'
|
|
});
|
|
}
|
|
}
|
|
|
|
function reportException(exception) {
|
|
|
|
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);
|
|
|
|
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.');
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
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 (params.type === 'nonBillable') {
|
|
var nonBillableSpans = filterSpans(spans, {type: 'nonBillable', reason: params.reason});
|
|
var nonBillableStatus = spansStatus(nonBillableSpans);
|
|
|
|
if (nonBillableStatus !== 'clockedIn') {
|
|
return Promise.reject('Not clocked in');
|
|
}
|
|
|
|
var span = ensureSingularSpan(filterSpans(nonBillableSpans, {status: 'open'}));
|
|
}
|
|
|
|
span.status = 'closed';
|
|
span.end = now.clone().utc().toDate();
|
|
span.duration = moment(span.end).diff(span.start, 'seconds');
|
|
span.notes = params.notes;
|
|
|
|
findUserSpansForWeek(user, now)
|
|
.then(weeklySpans => handleClockOutExceptions(span, user, weeklySpans, now));
|
|
|
|
return span.save().then(spanToResponse);
|
|
}
|
|
|
|
function handleWorkorderDetailsRequest(params, user, spans, workorder, today) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
return workorder;
|
|
}
|
|
|
|
function spanToResponse(span) {
|
|
return {
|
|
start: span.start,
|
|
end: span.end,
|
|
duration: span.duration
|
|
};
|
|
}
|
|
|
|
function ensureSingularSpan(spans) {
|
|
if (spans.length != 1) {
|
|
throw new MultipleSpansError(spans);
|
|
}
|
|
|
|
return spans[0];
|
|
}
|
|
|
|
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);
|
|
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 validateDate(req, field) {
|
|
let date = req.param(field);
|
|
|
|
if (!date) {
|
|
return Promise.reject(`Parameter '${field}' is required.`);
|
|
}
|
|
|
|
date = moment(date, 'YYYY-MM-DD');
|
|
if (!date.isValid()) {
|
|
return Promise.reject(`Parameter '${field}' is not a valid date.`)
|
|
}
|
|
|
|
return Promise.resolve(date);
|
|
}
|
|
|
|
function sendApprovalRequiredResponse(res, date) {
|
|
date = moment().startOf('week').subtract(1, 'week');
|
|
res.json({
|
|
tasks: [{
|
|
type: 'approveTimesheet',
|
|
week: date.format('YYYY-MM-DD')
|
|
}]
|
|
});
|
|
}
|
|
|
|
module.exports = function () {
|
|
return {
|
|
index: function (req, res) {
|
|
|
|
//TODO: Check to make sure user has a valid timesheet.
|
|
|
|
var today = moment();
|
|
|
|
hasTechApprovedPreviousWeek(req.user.id, today)
|
|
.then(approved => {
|
|
console.log('Approved? ', approved);
|
|
if (!approved) {
|
|
return sendApprovalRequiredResponse(res, today);
|
|
}
|
|
|
|
var spans = findUserSpans(req.user.id, today);
|
|
var workorders = findUserWorkorders(req.user, today);
|
|
|
|
Promise.join(spans, workorders, handleStatusRequest)
|
|
.then(responseHandler(res))
|
|
.catch(errorHandler(res));
|
|
});
|
|
},
|
|
|
|
clockIn: function (req, res) {
|
|
|
|
//TODO: Check to make sure user has a valid timesheet.
|
|
|
|
var today = moment();
|
|
|
|
hasTechApprovedPreviousWeek(req.user.id, today)
|
|
.then(approved => {
|
|
|
|
if (!approved) {
|
|
return sendApprovalRequiredResponse(res, today);
|
|
}
|
|
|
|
var params = validateClockRequest(req);
|
|
var spans = findUserSpans(req.user.id, today);
|
|
var workorders = findUserWorkorders(req.user, today);
|
|
|
|
Promise.join(params, req.user, spans, workorders, today, handleClockInRequest)
|
|
.then(responseHandler(res))
|
|
.catch(errorHandler(res));
|
|
});
|
|
},
|
|
|
|
clockOut: function (req, res) {
|
|
|
|
//TODO: Check to make sure user has a valid timesheet.
|
|
|
|
var today = moment();
|
|
|
|
hasTechApprovedPreviousWeek(req.user.id, today)
|
|
.then(approved => {
|
|
|
|
if (!approved) {
|
|
return sendApprovalRequiredResponse(res, today);
|
|
}
|
|
|
|
Promise
|
|
.props({
|
|
id: req.body.id,
|
|
date: moment(),
|
|
notes: req.body.notes,
|
|
reason: req.body.reason,
|
|
type: req.body.type
|
|
})
|
|
.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));
|
|
},
|
|
|
|
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));
|
|
},
|
|
|
|
}
|
|
};
|