Commit ef75235e authored by Farid Neshat's avatar Farid Neshat

Add approve timeoff

parent 81b91378
Pipeline #399 failed with stages
in 16 seconds
This diff is collapsed.
......@@ -21,9 +21,11 @@
"bluebird": "^3.5.0",
"coffee-script": "^1.12.7",
"hubot-conversation": "^1.1.1",
"hubot-slack-interactive-messages": "0.0.1",
"moment": "^2.18.1",
"moment-business-time": "^0.7.0",
"node-bamboohr": "^1.0.2",
"outbound-http-logger": "^1.0.3",
"sugar": "^2.0.4",
"timestring": "^5.0.0"
},
......@@ -36,7 +38,8 @@
"hubot-test-helper": "^1.7.0",
"mocha": "^3.4.2",
"sinon": "^2.4.1",
"sinon-chai": "^2.7.0"
"sinon-chai": "^2.7.0",
"supertest": "^3.0.0"
},
"main": "index.coffee",
"scripts": {
......
......@@ -7,4 +7,4 @@ module.exports = new Bamboohr({
subdomain: process.env.HUBOT_BAMBOOHR_DOMAIN,
});
bluebird.promisifyAll([Bamboohr, require('node-bamboohr/lib/employee')]);
bluebird.promisifyAll([Bamboohr, require('node-bamboohr/lib/employee'), require('node-bamboohr/lib/time-off-request')]);
......@@ -15,16 +15,15 @@
'use strict';
const Sugar = require('sugar');
const { Object, Date } = Sugar;
const { Object, Date, Array } = Sugar;
const bamboohr = require('../bamboohr');
const types = require('../types');
const utils = require('../utils');
const parseArgs = require('../parse-args');
const assert = require('assert');
module.exports = function(robot) {
robot.respond(/show *(.+?) *time-?offs?\s*(.*?)\s*$/i, res => {
let [, keywords, args] = res.match;
robot.respond(/(show|approve) *(.+?) *time-?offs?\s*(\d+)?\s*(.*?)\s*$/i, res => {
let [, action, keywords, id, args] = res.match;
let { all, type, status, lastOnly, mineOnly, employeeSearch } = parseShowKeywords(keywords);
bamboohr.employeesAsync().then(employees => {
......@@ -75,6 +74,11 @@ module.exports = function(robot) {
query.push('time-offs');
if (id) {
query.push('with id of', id);
options.id = parseFloat(id);
}
if (args) {
args = parseArgs(/\b(on|from|to|untill|till)\b/gi, args);
......@@ -117,35 +121,120 @@ module.exports = function(robot) {
if (lastOnly) {
requests = requests.slice(-1);
}
requests.forEach(request => {
let sentence = [];
if (!mineOnly) {
const employee = employees.find(employee => employee.id === request.employeeId);
let name;
if (employee) {
const { fields } = employee;
name = fields.preferredName ||
fields.firstName ||
fields.lastName ||
fields.displayName;
requests.forEach(request => {
let sentence = [];
if (!mineOnly || action === 'approve') {
const employee = employees.find(employee => employee.id === request.employeeId);
let name;
if (employee) {
const { fields } = employee;
name = fields.preferredName ||
fields.firstName ||
fields.lastName ||
fields.displayName;
} else {
name = request.employeeName;
}
sentence.push(name, 'has');
}
let { status } = request;
if (action === 'approve') {
sentence.push(status);
// So id won't be printed
const requestFromatted = utils.formatTimeOff({ ...request, id: null });
sentence.push(/[aeiou]/.test(requestFromatted[0]) ? 'an' : 'a');
sentence.push(requestFromatted);
} else {
name = request.employeeName;
sentence.push(/[aeiou]/.test(request.status[0]) ? 'an' : 'a');
if (status === 'requested') {
status = 'pending';
}
sentence.push(status);
sentence.push(utils.formatTimeOff(request));
}
sentence.push(name, 'has');
}
sentence.push(/[aeiou]/.test(request.status[0]) ? 'an' : 'a');
let { status } = request;
if (status === 'requested') {
status = 'pending';
}
sentence.push(status);
sentence.push(utils.formatTimeOff(request));
res.send(sentence.join(' '));
});
sentence = sentence.join(' ');
if (action === 'approve') {
res.send({
text: "Here's the current pending requests:",
attachments: requests.map(req => ({
text: sentence,
mrkdwn_in: ['text'],
color: '#3AA3E3',
callback_id: 'timeoffRequest#' + req.id,
actions: [
{
"name": "approved",
"text": "Approve",
"type": "button",
"style": "primary"
},
{
"name": "rejected",
"text": "Reject",
"type": "button",
"style": "danger"
}
]
})).concat({
"color": "#36a64f",
"title": "View all in calendar",
"title_link": "https://olindata.bamboohr.co.uk/calendar",
"callback_id": 'timeoffRequest#all',
"actions": [
{
"name": "approved",
"text": "Approve All",
"type": "button",
"style": "primary"
},
{
"name": "rejected",
"text": "Reject All",
"type": "button",
"style": "danger"
}
]
})
})
} else {
res.send(sentence);
}
});
});
});
}).catch(utils.errorHandler(res));
});
robot.setActionHandler(/timeoffRequest#/, payload =>
Promise.all(payload.original_message.attachments.map(attachment => {
if (payload.callback_id === attachment.callback_id || payload.callback_id === 'timeoffRequest#all') {
const { name } = payload.actions[0];
if (attachment.callback_id === 'timeoffRequest#all') {
return {
...attachment,
text: `${attachment.text}\n*You have ${name} all timeoffs.*`,
actions: [],
};
}
return bamboohr.timeOffRequest(attachment.callback_id.split('#')[1])
.changeStatusAsync({status: name})
.then(() => `You have ${name} this`,
'Something went wrong. Try using BambooHR itself. :(')
.then(msg => ({
...attachment,
text: `${attachment.text}\n*${msg}*`,
actions: [],
}));
}
return attachment;
})).then(attachments => ({...payload.original_message, attachments})));
};
const statuses = ["approved", "denied", "superceded", "pending", "requested", "canceled"];
......
......@@ -119,6 +119,15 @@ I will try to guess the end date and the period, but in case if I was wrong, use
request sick leave from tomorrow till saturday for 20 hours because I got the flu.
The possible types are: ${types.timeOffTypes.map(type => type.shortName).join(', ')}
You can also view timeoffs via `show timeoffs`. You can filter by type, status, or the requeste:
show Farid's approved sick leave timeoffs
And you can also specify a time range:
show timeoffs from tomorrow to thursday
show timeoffs on thursday
`)
});
};
......
This diff is collapsed.
// Description
// Just a mock
'use strict';
const sinon = require('sinon');
module.exports = robot => {
robot.setActionHandler = sinon.spy();
robot.setOptionsHandler = sinon.spy();
};
......@@ -34,20 +34,20 @@ describe('Request', function() {
describe('requesting timeoff', function () {
it('from for', function() {
return this.room.user.say('bob', 'hubot request timeoff from 25 July 2017 for 2 days').then(() => {
expect(this.room.messages[1][1]).to.be.equal('@bob So you want vacation from Tue Jul 25' +
' 2017 to Wed Jul 26 for 16 working hours?');
expect(this.room.messages[1][1]).to.be.equal('@bob So you want vacation from *Tue Jul 25' +
' 2017* to *Wed Jul 26* for 16 working hours?');
});
});
it('from for because', function() {
return this.room.user.say('bob', 'hubot request timeoff from 25 July 2017 for 2 days because fuck').then(() => {
expect(this.room.messages[1][1]).to.be.equal('@bob So you want vacation from Tue Jul 25' +
' 2017 to Wed Jul 26 for 16 working hours?');
expect(this.room.messages[1][1]).to.be.equal('@bob So you want vacation from *Tue Jul 25' +
' 2017* to *Wed Jul 26* for 16 working hours?');
});
});
it('from for 8 hours because', function() {
return this.room.user.say('bob', 'hubot request timeoff from 25 July 2017 for 8 hours because fuck').then(() => {
expect(this.room.messages[1][1]).to.be.equal('@bob So you want vacation from Tue Jul 25' +
' 2017 to Tue Jul 25 for 8 working hours?');
expect(this.room.messages[1][1]).to.be.equal('@bob So you want vacation from *Tue Jul 25' +
' 2017* to *Tue Jul 25* for 8 working hours?');
});
});
});
......
'use strict';
const { Array } = require('sugar');
require('coffee-script/register');
const Helper = require('hubot-test-helper');
const sinon = require('sinon');
const chai = require('chai');
chai.use(require('sinon-chai'));
const { expect } = chai;
// Must be required before the script to stub everything properly
const stubs = require('./stubs');
const Bamboohr = require('node-bamboohr');
const sctiptPath = '../src/scripts/show.js';
const script = require(sctiptPath);
const helper = new Helper(sctiptPath);
describe('Show', function() {
before(function () {
this.clock = sinon.useFakeTimers(Date.parse('1 July 2017'));
Bamboohr.prototype.timeOffRequests.callsFake(function (options, cb) {
// We delete the start and end so the Array.filter will still work
options = { ...options };
delete options.start;
delete options.end;
if (options.type) {
options = { ...options, type: 'Sick Leave' };
}
cb(null, Array.filter([{
start: '2011-10-02',
end: '2011-10-01',
created: '2011-10-02',
type: 'Vacation',
amount: 5,
amountUnit: 'days',
status: 'requested',
employeeId: stubs.employeeId,
}, {
start: '2011-10-03',
end: '2011-10-01',
created: '2011-10-02',
type: 'Sick Leave',
amount: 5,
amountUnit: 'days',
status: 'requested',
employeeId: stubs.employeeId,
}, {
start: '2011-10-01',
end: '2011-10-01',
created: '2011-10-01',
type: 'Vacation',
amount: 5,
amountUnit: 'days',
status: 'approved',
employeeId: stubs.employeeId,
}], options));
});
});
after(function () {
this.clock.restore();
Bamboohr.prototype.timeOffRequests.resetBehavior();
});
beforeEach(function() {
this.room = helper.createRoom({
name: 'bob',
});
});
afterEach(function() {
this.room.destroy();
});
describe('Parsing keywords', function () {
it('nothing', function () {
expect(script.parseShowKeywords('')).to.be.eql({
mineOnly: false,
lastOnly: false,
status: 'pending',
all: false,
});
});
it('mine', function () {
expect(script.parseShowKeywords('my')).to.be.eql({
mineOnly: true,
lastOnly: false,
status: 'pending',
all: false,
});
});
it('pending', function () {
expect(script.parseShowKeywords('my')).to.be.eql({
mineOnly: true,
lastOnly: false,
status: 'pending',
all: false,
});
});
it('last', function () {
expect(script.parseShowKeywords('last')).to.be.eql({
mineOnly: false,
lastOnly: true,
status: 'pending',
all: false,
});
});
it('other status', function () {
expect(script.parseShowKeywords('my denied')).to.be.eql({
mineOnly: true,
lastOnly: false,
status: 'denied',
all: false,
});
});
it('with type', function () {
expect(script.parseShowKeywords('vacation')).to.nested.include({
mineOnly: false,
lastOnly: false,
status: 'pending',
'type.shortName': 'vacation',
all: false,
});
expect(script.parseShowKeywords('sick leave')).to.nested.include({
mineOnly: false,
lastOnly: false,
status: 'pending',
'type.shortName': 'sick',
all: false,
});
});
it('all', function () {
expect(script.parseShowKeywords('all')).to.be.eql({
mineOnly: false,
lastOnly: false,
status: undefined,
all: true,
});
});
});
describe('show timeoffs', function () {
it('simple', function () {
return this.room.user.say('bob', 'hubot show time-offs').then(() => {
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
status: 'requested',
start: new Date('2017/07/01'),
});
expect(this.room.messages).to.have.lengthOf(4).and.nested.property('2.1').to.be.equal(
'Farid has a pending vacation from Sun Oct 2 2011 to Sat Oct 1 for 5 working days'
);
expect(this.room.messages[1][1]).to.be.equal('Searching for pending time-offs');
});
});
it('all', function () {
return this.room.user.say('bob', 'hubot show all time-offs').then(() => {
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
start: new Date('2017/07/01'),
});
expect(this.room.messages[1][1]).to.be.equal('Searching for time-offs');
expect(this.room.messages[2][1]).to.be.equal(
'Farid has a pending vacation from Sun Oct 2 2011 to Sat Oct 1 for 5 working days');
expect(this.room.messages[4][1]).to.be.equal(
'Farid has an approved vacation from Sat Oct 1 2011 to Sat Oct 1 for 5 working days');
});
});
it('last', function () {
return this.room.user.say('bob', 'hubot show last time-off').then(() => {
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
status: 'requested',
start: new Date('2017/07/01'),
});
expect(this.room.messages).to.have.lengthOf(3).and.nested.property('2.1').to.be.equal(
'Farid has a pending sick leave from Mon Oct 3 2011 to Sat Oct 1 for 5 working days');
});
});
it('with type', function () {
return this.room.user.say('bob', 'hubot show sick leave time-off').then(() => {
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
status: 'requested',
type: 2,
start: new Date('2017/07/01'),
});
expect(this.room.messages[1][1]).to.be.equal('Searching for pending sick leave time-offs');
expect(this.room.messages).to.have.lengthOf(3).and.nested.property('2.1').to.be.equal(
'Farid has a pending sick leave from Mon Oct 3 2011 to Sat Oct 1 for 5 working days');
});
});
describe('with arguments', function () {
it('on', function () {
return this.room.user.say('bob', 'hubot show leave time-off on today').then(() => {
expect(this.room.messages[1][1]).to.be.equal('Searching for pending time-offs on Sat Jul 1');
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
status: 'requested',
start: new Date('2017/07/01'),
end: new Date('2017/07/01'),
});
});
});
it('from', function () {
return this.room.user.say('bob', 'hubot show leave time-off from tomorrow').then(() => {
expect(this.room.messages[1][1]).to.be.equal('Searching for pending time-offs from Sat Jul 1');
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
status: 'requested',
start: new Date('2017/07/02'),
});
});
});
it('to', function () {
return this.room.user.say('bob', 'hubot show leave time-off to tomorrow').then(() => {
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
status: 'requested',
start: new Date('2017/07/01'),
end: new Date('2017/07/02'),
});
});
});
it('mine', function () {
return this.room.user.say('bob', 'hubot show my time-off', {
profile: { email: stubs.email }
}).then(() => {
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
status: 'requested',
employeeId: stubs.employeeId,
start: new Date('2017/07/01'),
});
expect(this.room.messages).to.have.lengthOf(4).and.nested.property('2.1').to.be.equal(
'a pending vacation from Sun Oct 2 2011 to Sat Oct 1 for 5 working days');
});
});
it('pass a name', function () {
return this.room.user.say('bob', `hubot show ${stubs.name}'s time-off`).then(() => {
expect(this.room.messages[1][1]).to.be.equal(`Searching for pending ${stubs.email}'s time-offs`);
expect(Bamboohr.prototype.timeOffRequests).to.have.been.calledWith({
status: 'requested',
employeeId: stubs.employeeId,
start: new Date('2017/07/01'),
});
expect(this.room.messages).to.have.lengthOf(4).and.nested.property('2.1').to.be.equal(
'Farid has a pending vacation from Sun Oct 2 2011 to Sat Oct 1 for 5 working days');
});
});
});
});
});
{
"actions": [
{
"name": "approved"
}
],
"callback_id": "timeoffRequest#2",
"team": {
"id": "T012AB0A1",
"domain": "pocket-calculator"
},
"channel": {
"id": "C012AB3CD",
"name": "general"
},
"user": {
"id": "U012A1BCD",
"name": "musik"
},
"action_ts": "1481579588.685999",
"message_ts": "1481579582.000003",
"attachment_id": "1",
"token": "iUeRJkkRC9RMMvSRTd8gdq2m",
"original_message": {
"text": "Here's the current pending requests:",
"attachments": [
{
"text": "Farid has requested a sick leave from *Mon Oct 3 2011* to *Sat Oct 1* for 5 working days",
"mrkdwn_in": ["text"],
"color": "#3AA3E3",
"callback_id": "timeoffRequest#2",
"actions": [
{
"name": "approved",
"text": "Approve",
"type": "button",
"style": "primary"
},
{
"name": "rejected",
"text": "Reject",
"type": "button",
"style": "danger"
}
]
},
{
"text": "Farid has requested a sick leave from *Mon Oct 3 2011* to *Sat Oct 1* for 5 working days",
"mrkdwn_in": ["text"],
"color": "#3AA3E3",
"callback_id": "timeoffRequest#3",
"actions": [
{
"name": "approved",
"text": "Approve",
"type": "button",
"style": "primary"
},
{
"name": "rejected",
"text": "Reject",
"type": "button",
"style": "danger"
}
]
},
{
"color": "#36a64f",
"title": "View all in calendar",
"title_link": "https://olindata.bamboohr.co.uk/calendar",
"callback_id": "timeoffRequest#all",
"actions": [
{
"name": "approved",
"text": "Approve All",
"type": "button",
"style": "primary"
},
{
"name": "rejected",
"text": "Reject All",
"type": "button",
"style": "danger"
}
]
}
]
},
"response_url": "https://hooks.slack.com/actions/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ"
}
......@@ -2,8 +2,8 @@
const Bamboohr = require('node-bamboohr');
const BambooEmployee = require('node-bamboohr/lib/employee');
const BambooTimeoff = require('node-bamboohr/lib/time-off-request');
const sinon = require("sinon");
const { Array } = require('sugar');
exports.email = 'some@gma.com';
exports.employeeId = 2;
......@@ -34,6 +34,7 @@ const stubs = [
})]);
}),
sinon.stub(Bamboohr.prototype, 'timeOffRequests'),
sinon.stub(BambooTimeoff.prototype, 'changeStatus').yields(),
];
beforeEach(function () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment