From 715e9182d376e31b35c146766143fdc1af95e68e Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 2 Nov 2017 17:19:59 +0000 Subject: [PATCH 001/539] Ditch support of node.js versions <4.0.0 --- .travis.yml | 2 -- README.md | 2 +- package.json | 5 ++++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8c5e501c6..760f3335e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: node_js node_js: - - "0.10" - - "0.12" - "v4.8.3" - "v6.10.1" sudo: false diff --git a/README.md b/README.md index 1233c569a..57348ce55 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Create company account and use cloud based version. Install TimeOff.Management application within your infrastructure: -(make sure you have Node.js and SQLite installed) +(make sure you have Node.js (>=4.0.0) and SQLite installed) ```bash git clone https://github.com/timeoff-management/application.git timeoff-management diff --git a/package.json b/package.json index efe3b3fde..e1378fb2b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "TimeOff.Management", - "version": "0.6.2", + "version": "0.7.0", "private": false, "description": "Simple yet powerful absence management software for small and medium size business", + "engines": { + "node": ">=4.0.0" + }, "dependencies": { "bluebird": "^2.10.2", "body-parser": "^1.8.4", From 8565b35812d10673b87b4926e828c6e30d5587ec Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 3 Nov 2017 12:17:08 +0000 Subject: [PATCH 002/539] Employe absence page to show leave statistics by leave types --- lib/route/users.js | 21 ++++++++++----------- views/partials/user_details/absences.hbs | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/route/users.js b/lib/route/users.js index 144f1e786..d7625b135 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -151,18 +151,16 @@ router.get('/edit/:user_id/absences/', function(req, res){ '/js/inittooltips.js' ); - Promise.try(function(){ - ensure_user_id_is_integer({req : req, user_id : user_id}); - }) - .then(function(){ - return req.user.get_company_for_user_details({ - user_id : user_id, - }); - }) - .then(function(company){ - - var employee = company.users[0]; + Promise + .try( () => ensure_user_id_is_integer({req : req, user_id : user_id}) ) + .then(() => req.user.get_company_for_user_details({ user_id : user_id }) ) + .then(function(company){ + let employee = company.users[0]; + return employee.reload_with_session_details(); + }) + .then( employee => employee.reload_with_leave_details({})) + .then(employee => { return employee .promise_schedule_I_obey() .then(function(){ @@ -173,6 +171,7 @@ router.get('/edit/:user_id/absences/', function(req, res){ employee : employee, leaves : leaves, show_absence_tab : true, + leave_type_statistics : employee.get_leave_statistics_by_types(), }); }); }); diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index c2cbf284b..952f0e605 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -19,6 +19,20 @@ +
+
+
Used so far
+ {{# if leave_type_statistics }} + {{# each leave_type_statistics }} +
{{this.leave_type.name}}: {{this.days_taken}}{{# if this.limit}} out of {{this.limit}}{{/if}}
+ {{/each}} + {{else}} +
No approved requests so far.
+ {{/if}} +
+
+ +
From b2a2f658f6164c73dc2f87daa7231cb6ac855040 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 3 Nov 2017 13:00:55 +0000 Subject: [PATCH 003/539] Littele bit better visualisation of remaining leaves --- lib/route/users.js | 17 +++++++++++++++++ views/partials/user_details/absences.hbs | 21 +++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/route/users.js b/lib/route/users.js index d7625b135..5323258bb 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -161,6 +161,22 @@ router.get('/edit/:user_id/absences/', function(req, res){ }) .then( employee => employee.reload_with_leave_details({})) .then(employee => { + + let leave_statistics = { + total_for_current_year : employee.calculate_total_number_of_days_n_allowance(), + remaining : employee.calculate_number_of_days_available_in_allowance(), + }; + + leave_statistics.used_so_far = leave_statistics.total_for_current_year - leave_statistics.remaining; + + leave_statistics.used_so_far_percent = leave_statistics.total_for_current_year > 0 + ? 100 * leave_statistics.used_so_far / leave_statistics.total_for_current_year + : 0; + + leave_statistics.remaining_percent = leave_statistics.total_for_current_year > 0 + ? 100 * leave_statistics.remaining / leave_statistics.total_for_current_year + : 0; + return employee .promise_schedule_I_obey() .then(function(){ @@ -172,6 +188,7 @@ router.get('/edit/:user_id/absences/', function(req, res){ leaves : leaves, show_absence_tab : true, leave_type_statistics : employee.get_leave_statistics_by_types(), + leave_statistics : leave_statistics, }); }); }); diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index 952f0e605..7c10cd7c7 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -13,15 +13,28 @@
- -
- + +
+
+ {{ leave_statistics.used_so_far }} days used so far +
+
+ {{ leave_statistics.remaining }} days remaining in current year +
+ +
+ + +
+ + +

{{ leave_statistics.remaining }} out of {{ leave_statistics.total_for_current_year }}

-
Used so far
+
Absences used this year grouped by leave types
{{# if leave_type_statistics }} {{# each leave_type_statistics }}
{{this.leave_type.name}}: {{this.days_taken}}{{# if this.limit}} out of {{this.limit}}{{/if}}
From 5d870a6e378df1112397fce2379415846ba31689 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 3 Nov 2017 13:14:27 +0000 Subject: [PATCH 004/539] Add tooltips to exmplain in detail about absence statistics --- public/js/global.js | 4 ++++ views/partials/user_details/absences.hbs | 22 +++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/public/js/global.js b/public/js/global.js index b318ee78b..533df54dc 100644 --- a/public/js/global.js +++ b/public/js/global.js @@ -55,3 +55,7 @@ $(document).ready(function(){ format : "dd/mm/yyyy" } }(jQuery); + +$(function () { + $('[data-toggle="popover"]').popover() +}) diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index 7c10cd7c7..5336b7689 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -15,17 +15,33 @@
-
+
{{ leave_statistics.used_so_far }} days used so far
-
+
{{ leave_statistics.remaining }} days remaining in current year
- + + +
From eedc838462354b47ff96ba4aa66edf9ea781098c Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 3 Nov 2017 13:43:30 +0000 Subject: [PATCH 005/539] Rever deletion of hidden element used in tests --- views/partials/user_details/absences.hbs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index 5336b7689..b4f49d2db 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -36,16 +36,12 @@ {{ leave_statistics.remaining }} days remaining in current year
-
- - -
-

{{ leave_statistics.remaining }} out of {{ leave_statistics.total_for_current_year }}

+
@@ -61,7 +57,6 @@
-
From 5bf4564b6de2de41e9936d9d08453f4a9cd8ba17 Mon Sep 17 00:00:00 2001 From: Bernard Badjari Date: Thu, 9 Nov 2017 21:22:33 -0500 Subject: [PATCH 006/539] Fixed some spelling and grammar in README.md file. Also removed duplicate information. --- README.md | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 38a794171..2e770608e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # TimeOff.Management -Web application for managing employees absence. +Web application for managing employee absences. [![Stories in Ready](https://badge.waffle.io/timeoff-management/application.png?label=ready&title=Ready)](https://waffle.io/timeoff-management/application) @@ -9,7 +9,7 @@ Web application for managing employees absence. ## Features -**Multiple views of staff absence** +**Multiple views of staff absences** Calendar view, Team view, or Just plain list. @@ -17,25 +17,25 @@ Calendar view, Team view, or Just plain list. Add custom absence types: Sickness, Maternity, Working from home, Birthday etc. Define if each uses vacation allowance. -Optionally limit the amount of days employee can take for each Leave type. E.g. no more than 10 days of Sick days per year. +Optionally limit the amount of days employees can take for each Leave type. E.g. no more than 10 Sick days per year. Setup public holidays as well as company specific days off. -Group employees by departments: bring your organisation structure into, set the supervisor for every department. +Group employees by departments: bring your organisational structure, set the supervisor for every department. Customisable working schedule for company and individuals. **Third Party Calendar Integration** -Broadcast employees whereabout into external calendar providers: MS Outlook, Google Calendar, and iCal. +Broadcast employee whereabouts into external calendar providers: MS Outlook, Google Calendar, and iCal. -Create calendar feeds for individual, departments or entire company. +Create calendar feeds for individuals, departments or entire company. **Three Steps Workflow** -Employee request time off or revoke existing one. +Employee requests time off or revokes existing one. -Supervisor get email notification and decide about upcoming employee absence. +Supervisor gets email notification and decides about upcoming employee absence. Absence is accounted. Peers are informed via team view or calendar feeds. @@ -43,15 +43,13 @@ Absence is accounted. Peers are informed via team view or calendar feeds. There are following types of users: employees, supervisors, and administrators. -Optional LDAP authentification: configure applicationto use your LDAP server for user authentication. +Optional LDAP authentication: configure application to use your LDAP server for user authentication. **Seamless data migration betweeen different installations** -User friendly and simple work-flow for data migration between different TimeOff.Management installations. +User friendly and simple workflow for data migration between different TimeOff.Management installations. -Admin user can download the entire company data as a single JSON file. - -And then restore the account at different installation by simply uploading the JSON. +Admin user can download the entire company data as a single JSON file and then restore the account at a different installation by simply uploading the JSON. **Works on mobile phones** @@ -63,16 +61,14 @@ The most used customer paths are mobile friendly: **Lots of other little things that would make life easier** -Manually adjust employees allowance +Manually adjust employee allowances e.g. employee has extra day in lieu. -Upon creation employee receives pro rata vacation allowance, depending on start date. - -Users of three types: admins, supervisors, and employees. +Upon creation employee receives pro-rated vacation allowance, depending on start date. Email notification to all involved parties. -Optionally allow employees to see the time off information of entire company regardless the department structure. +Optionally allow employees to see the time off information of entire company regardless of department structure. ## Screenshots @@ -114,7 +110,7 @@ npm test (make sure that application with default settings is up and running) -Any bugfixes or enhancements should have good test coverage to get them into "master" branch. +Any bug fixes or enhancements should have good test coverage to get them into "master" branch. ## Updating existing instance with new code @@ -130,5 +126,4 @@ npm start ## Feedback -Please report any issues or feedback to twitter or Email: pavlo at timeoff.management - +Please report any issues or feedback to twitter or Email: pavlo at timeoff.management \ No newline at end of file From de9c8c54b81709f20e392c8b50046e93ebb74394 Mon Sep 17 00:00:00 2001 From: Bernard Badjari Date: Thu, 9 Nov 2017 21:28:17 -0500 Subject: [PATCH 007/539] Minor spelling correction. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e770608e..af8d5262d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Supervisor gets email notification and decides about upcoming employee absence. Absence is accounted. Peers are informed via team view or calendar feeds. -**Accesss control** +**Access control** There are following types of users: employees, supervisors, and administrators. From 90f03478c545dfeea725f85d236909adf6c89b5d Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Mon, 6 Nov 2017 20:08:09 +0000 Subject: [PATCH 008/539] Prepare bsic front end --- public/css/style.css | 9 +++++++++ scss/main.scss | 13 +++++++++++-- views/team_view.hbs | 7 +++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index e82a3d6fb..c195d4521 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -86,6 +86,14 @@ td.leave_cell_pended { padding-top: 5px; padding-bottom: 5px; } +.team-view-users .user-name-cell { + text-overflow: ellipsis; + /* Cannot avoid specifying width with pixels... */ + width: 165px; + white-space: nowrap; + overflow: hidden; + display: inline-block; } + td.team-view-header { height: 43px; } @@ -158,6 +166,7 @@ div.feeds-holder { body.modal-open { position: fixed; } } +/* To be used for selected items in lists */ .selected-item { background: #F5F7F8; font-weight: bolder; } diff --git a/scss/main.scss b/scss/main.scss index 596598c03..116005ffa 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -106,8 +106,17 @@ td.leave_cell_pended { max-width : 450px; } .team-view-users td { - padding-top : 5px; - padding-bottom : 5px; + padding-top : 5px; + padding-bottom : 5px; + +} +.team-view-users .user-name-cell { + text-overflow: ellipsis; + /* Cannot avoid specifying width with pixels... */ + width: 165px; + white-space: nowrap; + overflow: hidden; + display: inline-block; } td.team-view-header { height: 43px; diff --git a/views/team_view.hbs b/views/team_view.hbs index a8225eceb..76f0ecf0e 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -28,7 +28,7 @@
- {{# each users_and_leaves}} - + + + + {{/each}}
+
{{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}}
{{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}}
From 099b59f444cc49945c7612e0f3725fa78a0127a2 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Mon, 6 Nov 2017 21:59:49 +0000 Subject: [PATCH 009/539] Row implementation --- lib/model/team_view.js | 41 +++++++++++++++++++++++++++++++++++++++++ lib/route/calendar.js | 25 ++++++++++++++----------- views/team_view.hbs | 2 +- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 4030063b3..66ccd72bd 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -87,4 +87,45 @@ TeamView.prototype.promise_team_view_details = function(args){ }; +TeamView.prototype.inject_statistics = function(args){ + let + team_view_details = args.team_view_details, + leave_types = args.leave_types || []; + + // Convert leave types array into look-up map + let leave_types_map = {}; + leave_types.forEach( lt => leave_types_map[lt.id] = lt ); + + + team_view_details + .users_and_leaves + .forEach(node => { + + let deducted_days = 0; + + node + .days + // Consider only those days that have any leave objects + .filter( day => !! day.leave_obj ) + .filter( day => !! day.leave_obj.is_approved_leave() ) + .forEach( day => { + let leave_type = leave_types_map[ day.leave_obj.leaveTypeId ]; + + if (! (leave_type && leave_type.use_allowance)) { + return; + } + + deducted_days++ + }); + + let statistics = { + deducted_days : deducted_days, + }; + + node.statistics = statistics; + }); + + return Promise.resolve( team_view_details ); +}; + module.exports = TeamView; diff --git a/lib/route/calendar.js b/lib/route/calendar.js index 15d4fed30..8b7f09fa5 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -158,17 +158,20 @@ router.get('/teamview/', function(req, res){ }), req.user.get_company_with_all_leave_types(), function(team_view_details, company){ - - return res.render('team_view', { - base_date : base_date, - prev_date : moment(base_date).add(-1,'month'), - next_date : moment(base_date).add(1,'month'), - users_and_leaves : team_view_details.users_and_leaves, - related_departments : team_view_details.related_departments, - current_department : team_view_details.current_department, - company : company, - }); - + return team_view.inject_statistics({ + team_view_details : team_view_details, + leave_types : company.leave_types, + }) + .then(team_view_details => res.render('team_view', { + base_date : base_date, + prev_date : moment(base_date).add(-1,'month'), + next_date : moment(base_date).add(1,'month'), + users_and_leaves : team_view_details.users_and_leaves, + related_departments : team_view_details.related_departments, + current_department : team_view_details.current_department, + company : company, + }) + ); }); }); diff --git a/views/team_view.hbs b/views/team_view.hbs index 76f0ecf0e..2db5dadf6 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -47,7 +47,7 @@ {{# each users_and_leaves}} {{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}} - + {{ statistics.deducted_days }} {{/each}} From b3ff990b8d09438ac7e4dd0fbce81059edc2d8cd Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 7 Nov 2017 15:53:26 +0000 Subject: [PATCH 010/539] Some more improvements --- lib/model/team_view.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 66ccd72bd..22727fc2e 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -107,7 +107,10 @@ TeamView.prototype.inject_statistics = function(args){ .days // Consider only those days that have any leave objects .filter( day => !! day.leave_obj ) + // Ignore those days which were not approved yet .filter( day => !! day.leave_obj.is_approved_leave() ) + .filter( day => ! day.is_weekend ) + .filter( day => ! day.is_bank_holiday ) .forEach( day => { let leave_type = leave_types_map[ day.leave_obj.leaveTypeId ]; @@ -115,7 +118,11 @@ TeamView.prototype.inject_statistics = function(args){ return; } - deducted_days++ + if (day.is_leave_afternoon && day.is_leave_morning){ + deducted_days++; + } else { + deducted_days = deducted_days + 0.5; + } }); let statistics = { From e9d8022fa552138b55cf5b0ff09f15dbe9621bcf Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 8 Nov 2017 07:15:28 +0000 Subject: [PATCH 011/539] Add new test to cover absence summary on team view --- lib/model/team_view.js | 2 + t/integration/team_view/deducted_column.js | 196 ++++++++++++++++++ .../{ => team_view}/teamview_cross_links.js | 14 +- 3 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 t/integration/team_view/deducted_column.js rename t/integration/{ => team_view}/teamview_cross_links.js (83%) diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 22727fc2e..552f09ef7 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -109,7 +109,9 @@ TeamView.prototype.inject_statistics = function(args){ .filter( day => !! day.leave_obj ) // Ignore those days which were not approved yet .filter( day => !! day.leave_obj.is_approved_leave() ) + // Ignore weekends .filter( day => ! day.is_weekend ) + // Ignore bank holidays .filter( day => ! day.is_bank_holiday ) .forEach( day => { let leave_type = leave_types_map[ day.leave_obj.leaveTypeId ]; diff --git a/t/integration/team_view/deducted_column.js b/t/integration/team_view/deducted_column.js new file mode 100644 index 000000000..aed44019d --- /dev/null +++ b/t/integration/team_view/deducted_column.js @@ -0,0 +1,196 @@ + +'use strict'; + +/* + * Scenario 1: + * + * Check that values for new columns are shown only for employess + * currently login user can supervise. + * + * The reason for this sceanrio is because in UK for instance it is illegal to share + * details for employees who are not supervisers. Peers should not know how many days + * their coleagues were off sick for instance. + * + * * create account by admin user A + * * add user B + * * add user C + * * ensure company has "Share absences between all employees" flag ON + * * make user B to be superviser of user C + * * login as user A and ensure team view shows deducted values for all three users + * * login as user B and ensure she sees deducted days only for user B (self) and user C + * but not for user A + * * login as user C and ensure she sees only values for her account + * + * */ + + +/* + * Scenario 2: + * + * Case when holidays spans through more then one month and is devided by bank holiday. + * + * * create new account as admin user A + * * create user B + * * create new bank holiday to be on 2 Aug 2016 + * * as user B create a holiday request from 28 July to morning of 4 Aug 2016 (a) + * + * * as user B create a holiday request from 12 Aug to 15 Aug 2016 (b) + * * as user B create a holiday request from 26 Aug to 2 Sep 2016 (d) + * * as user B create a sick day request from 18 to 18 Aug 2016 (e) + * * as user A approve leaves (a), (b), (d), and (e) + * * as user B cretae a holiday request from 24 Aug to 24 Aug 2016 (c) + * * navigate to team view and ensure that it shows 9.5 days were deducted for Aug 2016 + * * 2 days deducted for July 2016 + * * 2 days deducted for Sept 2016 + * + * */ + +var test = require('selenium-webdriver/testing'), + By = require('selenium-webdriver').By, + Promise = require("bluebird"), + expect = require('chai').expect, + add_new_user_func = require('../../lib/add_new_user'), + check_elements_func = require('../../lib/check_elements'), + config = require('../../lib/config'), + login_user_func = require('../../lib/login_with_user'), + logout_user_func = require('../../lib/logout_user'), + open_page_func = require('../../lib/open_page'), + register_new_user_func = require('../../lib/register_new_user'), + submit_form_func = require('../../lib/submit_form'), + user_info_func = require('../../lib/user_info'), + new_bankholiday_form_id= '#add_new_bank_holiday_form', + application_host = config.get_application_host(); + +describe('Case when holidays spans through more then one month and is devided by bank holiday', function(){ + + this.timeout( config.get_execution_timeout() ); + + let driver, + email_A, user_id_A, + email_B, user_id_B; + + it("Register new company as admin user A", function(done){ + register_new_user_func({ + application_host : application_host, + }) + .then(data => { + driver = data.driver; + email_A = data.email; + done(); + }); + }); + + it("Create second user B", function(done){ + add_new_user_func({ + application_host : application_host, + driver : driver, + }) + .then(data => { + email_B = data.new_user_email; + done(); + }); + }); + + it("Obtain information about user A", function(done){ + user_info_func({ + driver : driver, + email : email_A, + }) + .then(data => { + user_id_A = data.user.id; + done(); + }); + }); + + it("Obtain information about user B", function(done){ + user_info_func({ + driver : driver, + email : email_B, + }) + .then(data => { + user_id_B = data.user.id; + done(); + }); + }); + + it("Open page with bank holidays", function(done){ + open_page_func({ + url : application_host + 'settings/general/', + driver : driver, + }) + .then(function(){done()}); + }) + + it("Create new bank holiday to be on 2 Aug 2016", function(done){ + driver.findElement(By.css('#add_new_bank_holiday_btn')) + .then(el => el.click()) + .then(() => { + + // This is very important line when working with Bootstrap modals! + driver.sleep(1000); + + submit_form_func({ + driver : driver, + form_params : [{ + selector : new_bankholiday_form_id+' input[name="name__new"]', + value : 'Some summer holiday', + },{ + selector : new_bankholiday_form_id+' input[name="date__new"]', + value : '2016-08-02', + }], + submit_button_selector: new_bankholiday_form_id+' button[type="submit"]', + message : /Changes to bank holidays were saved/, + }) + .then(() => done()); + }); + }); + + it("Logout from user A (admin)", function(done){ + logout_user_func({ + application_host : application_host, + driver : driver, + }) + .then(() => done()); + }); + + it("Login as user B", function(done){ + login_user_func({ + application_host : application_host, + user_email : email_B, + driver : driver, + }) + .then(() => done()); + }); + + it("Open Book leave popup window", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then( el => el.click() ) + // This is very important line when working with Bootstrap modals! + .then( el => driver.sleep(1000) ) + .then(() => done()); + }); + + it("As user B create a holiday request from 28 July to morning of 4 Aug 2016 (a)", function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : 'select[name="from_date_part"]', + option_selector : 'option[value="2"]', + value : "2", + },{ + selector : 'input#from', + value : '2016-07-28', + },{ + selector : 'input#to', + value : '2016-08-04', + }], + message : /New leave request was added/, + }) + .then(function(){done()}); + }); + + after(function(done){ + console.log('>>> ' + email_B); + driver.quit().then(() => done()); + }); +}); diff --git a/t/integration/teamview_cross_links.js b/t/integration/team_view/teamview_cross_links.js similarity index 83% rename from t/integration/teamview_cross_links.js rename to t/integration/team_view/teamview_cross_links.js index f5ce95c50..a7f093f24 100644 --- a/t/integration/teamview_cross_links.js +++ b/t/integration/team_view/teamview_cross_links.js @@ -5,13 +5,13 @@ var test = require('selenium-webdriver/testing'), By = require('selenium-webdriver').By, expect = require('chai').expect, Promise = require("bluebird"), - register_new_user_func = require('../lib/register_new_user'), - login_user_func = require('../lib/login_with_user'), - open_page_func = require('../lib/open_page'), - add_new_user_func = require('../lib/add_new_user'), - logout_user_func = require('../lib/logout_user'), - check_teamview_func = require('../lib/teamview_check_user'), - config = require('../lib/config'), + register_new_user_func = require('../../lib/register_new_user'), + login_user_func = require('../../lib/login_with_user'), + open_page_func = require('../../lib/open_page'), + add_new_user_func = require('../../lib/add_new_user'), + logout_user_func = require('../../lib/logout_user'), + check_teamview_func = require('../../lib/teamview_check_user'), + config = require('../../lib/config'), application_host = config.get_application_host(); /* From 2d88b8750a1b9f3dccd64ac58fb98a52eb13e4e7 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 8 Nov 2017 10:09:43 +0000 Subject: [PATCH 012/539] Fix existing team view test --- .../{ => team_view}/basic_wall_chart.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename t/integration/{ => team_view}/basic_wall_chart.js (95%) diff --git a/t/integration/basic_wall_chart.js b/t/integration/team_view/basic_wall_chart.js similarity index 95% rename from t/integration/basic_wall_chart.js rename to t/integration/team_view/basic_wall_chart.js index 64a407954..e3f41a088 100644 --- a/t/integration/basic_wall_chart.js +++ b/t/integration/team_view/basic_wall_chart.js @@ -8,13 +8,13 @@ var test = require('selenium-webdriver/testing'), Promise = require("bluebird"), until = require('selenium-webdriver').until, _ = require('underscore'), - register_new_user_func = require('../lib/register_new_user'), - login_user_func = require('../lib/login_with_user'), - open_page_func = require('../lib/open_page'), - submit_form_func = require('../lib/submit_form'), - add_new_user_func = require('../lib/add_new_user'), - logout_user_func = require('../lib/logout_user'), - config = require('../lib/config'), + register_new_user_func = require('../../lib/register_new_user'), + login_user_func = require('../../lib/login_with_user'), + open_page_func = require('../../lib/open_page'), + submit_form_func = require('../../lib/submit_form'), + add_new_user_func = require('../../lib/add_new_user'), + logout_user_func = require('../../lib/logout_user'), + config = require('../../lib/config'), new_department_form_id = '#add_new_department_form', application_host = config.get_application_host(), company_edit_form_id ='#company_edit_form'; @@ -54,7 +54,7 @@ function check_teamview(data, emails){ }) .then(function(data){ var promise_to_check = data.driver - .findElements(By.css( 'tr.teamview-user-list-row > td' )) + .findElements(By.css( 'tr.teamview-user-list-row > td.user-name-cell' )) // Make sure that number of users is as expected .then(function(elements){ From cbc059d5172a95dc8bb1b3b665234514ecfad711 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 10 Nov 2017 07:28:18 +0000 Subject: [PATCH 013/539] Finish happy path test for deducted column on team view. --- lib/model/db/leave_type.js | 1 + t/integration/crud_leave_type.js | 6 +- t/integration/team_view/deducted_column.js | 199 ++++++++++++++++++++- t/lib/teamview_check_user.js | 2 +- views/team_view.hbs | 4 +- 5 files changed, 202 insertions(+), 10 deletions(-) diff --git a/lib/model/db/leave_type.js b/lib/model/db/leave_type.js index 6d900c4fc..322400b0c 100644 --- a/lib/model/db/leave_type.js +++ b/lib/model/db/leave_type.js @@ -43,6 +43,7 @@ module.exports = function(sequelize, DataTypes) { color : '#459FF3', companyId : company.id, limit : 10, + use_allowance : 0, }, ]) }, diff --git a/t/integration/crud_leave_type.js b/t/integration/crud_leave_type.js index 0eba8b1f0..c905386ae 100644 --- a/t/integration/crud_leave_type.js +++ b/t/integration/crud_leave_type.js @@ -76,7 +76,7 @@ describe('CRUD for leave types', function(){ },{ selector : leave_type_edit_form_id+' input[name="use_allowance__1"]', tick : true, - value : 'on', + value : 'off', }], }) .then(function(){ done() }); @@ -88,7 +88,7 @@ describe('CRUD for leave types', function(){ form_params : [{ selector : leave_type_edit_form_id+' input[name="use_allowance__1"]', tick : true, - value : 'off', + value : 'on', }], should_be_successful : true, submit_button_selector : leave_type_edit_form_id+' button[type="submit"]', @@ -106,7 +106,7 @@ describe('CRUD for leave types', function(){ tick : true, },{ selector : leave_type_edit_form_id+' input[name="use_allowance__1"]', - value : 'off', + value : 'on', tick : true, }], }) diff --git a/t/integration/team_view/deducted_column.js b/t/integration/team_view/deducted_column.js index aed44019d..a39b75d35 100644 --- a/t/integration/team_view/deducted_column.js +++ b/t/integration/team_view/deducted_column.js @@ -33,14 +33,13 @@ * * create user B * * create new bank holiday to be on 2 Aug 2016 * * as user B create a holiday request from 28 July to morning of 4 Aug 2016 (a) - * * * as user B create a holiday request from 12 Aug to 15 Aug 2016 (b) * * as user B create a holiday request from 26 Aug to 2 Sep 2016 (d) * * as user B create a sick day request from 18 to 18 Aug 2016 (e) * * as user A approve leaves (a), (b), (d), and (e) * * as user B cretae a holiday request from 24 Aug to 24 Aug 2016 (c) - * * navigate to team view and ensure that it shows 9.5 days were deducted for Aug 2016 - * * 2 days deducted for July 2016 + * * navigate to team view and ensure that it shows 9 days were deducted for Aug 2016 + * * 1.5 days deducted for July 2016 * * 2 days deducted for Sept 2016 * * */ @@ -189,8 +188,200 @@ describe('Case when holidays spans through more then one month and is devided by .then(function(){done()}); }); + it("Open Book leave popup window", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then( el => el.click() ) + // This is very important line when working with Bootstrap modals! + .then( el => driver.sleep(1000) ) + .then(() => done()); + }); + + it('As user B create a holiday request from 12 Aug to 15 Aug 2016 (b)', function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : 'input#from', + value : '2016-08-12', + },{ + selector : 'input#to', + value : '2016-08-15', + }], + message : /New leave request was added/, + }) + .then(function(){done()}); + }); + + it("Open Book leave popup window", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then( el => el.click() ) + // This is very important line when working with Bootstrap modals! + .then( el => driver.sleep(1000) ) + .then(() => done()); + }); + + it('As user B create a holiday request from 26 Aug to 2 Sep 2016', function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : 'input#from', + value : '2016-08-26', + },{ + selector : 'input#to', + value : '2016-09-02', + }], + message : /New leave request was added/, + }) + .then(() => done()); + }); + + it("Open Book leave popup window", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then( el => el.click() ) + // This is very important line when working with Bootstrap modals! + .then( el => driver.sleep(1000) ) + .then(() => done()); + }); + + it('As user B create a sick day request from 18 to 18 Aug 2016', function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : 'select[name="leave_type"]', + option_selector : 'option:nth-child(2)', + },{ + selector : 'input#from', + value : '2016-08-18', + },{ + selector : 'input#to', + value : '2016-08-18', + }], + message : /New leave request was added/, + }) + .then(() => done()); + }); + + it("Logout from user B", function(done){ + logout_user_func({ + application_host : application_host, + driver : driver, + }) + .then(() => done()); + }); + + it("Login as user A (admin)", function(done){ + login_user_func({ + application_host : application_host, + user_email : email_A, + driver : driver, + }) + .then(() => done()); + }); + + it("Open requests page", function( done ){ + open_page_func({ + url : application_host + 'requests/', + driver : driver, + }) + .then(() => done()); + }); + + it("Approve newly added leave request", function(done){ + let click_selector = `tr[vpp="pending_for__${email_B}"] .btn-success`; + driver + .findElement(By.css( click_selector )) + .then(el => el.click()) + .then(() => driver.findElement(By.css( click_selector ))) + .then(el => el.click()) + .then(() => driver.findElement(By.css( click_selector ))) + .then(el => el.click()) + .then(() => driver.findElement(By.css( click_selector ))) + .then(el => el.click()) + .then(() => done()); + }); + + it("Logout from user A (admin)", function(done){ + logout_user_func({ + application_host : application_host, + driver : driver, + }) + .then(() => done()); + }); + + it("Login as user B", function(done){ + login_user_func({ + application_host : application_host, + user_email : email_B, + driver : driver, + }) + .then(() => done()); + }); + + it("Open Book leave popup window", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then( el => el.click() ) + // This is very important line when working with Bootstrap modals! + .then( el => driver.sleep(1000) ) + .then(() => done()); + }); + + it('As user B cretae a holiday request from 24 Aug to 24 Aug 2016', function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : 'input#from', + value : '2016-08-24', + },{ + selector : 'input#to', + value : '2016-08-24', + }], + message : /New leave request was added/, + }) + .then(function(){done()}); + }); + + it('Navigate to team view and ensure that it shows 9 days were deducted for Aug 2016', function(done){ + open_page_func({ + url : application_host + 'calendar/teamview/?date=2016-08', + driver : driver, + }) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt, 'Ensure that system shows 9 days as deducted') + .to.be.eql('9'); + done(); + }); + }); + + it('1.5 days deducted for July 2016', function(done){ + open_page_func({ + url : application_host + 'calendar/teamview/?date=2016-07', + driver : driver, + }) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt, 'Ensure that system shows 1.5 days as deducted') + .to.be.eql('1.5'); + done(); + }); + }); + + it('2 days deducted for Sept 2016', function(done){ + open_page_func({ + url : application_host + 'calendar/teamview/?date=2016-09', + driver : driver, + }) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt, 'Ensure that system shows 2 days as deducted') + .to.be.eql('2'); + done(); + }); + }); + after(function(done){ - console.log('>>> ' + email_B); driver.quit().then(() => done()); }); }); diff --git a/t/lib/teamview_check_user.js b/t/lib/teamview_check_user.js index e254655fd..daa73ff31 100644 --- a/t/lib/teamview_check_user.js +++ b/t/lib/teamview_check_user.js @@ -1,6 +1,6 @@ /* - * Exporst function that checks if given emails of users are shown + * Exports function that checks if given emails of users are shown * on the Teamview page. And if so how they are rendered: as text or link. * * It does not check exact emails, just count numbers. diff --git a/views/team_view.hbs b/views/team_view.hbs index 2db5dadf6..442bcfff9 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -45,9 +45,9 @@ {{# each users_and_leaves}} - + {{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}} - {{ statistics.deducted_days }} + {{ statistics.deducted_days }} {{/each}} From 6ae8f62c3dacc274bb722e01ecba59995e091b06 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Mon, 13 Nov 2017 06:29:04 +0000 Subject: [PATCH 014/539] Add argument validation into newly added method --- lib/model/team_view.js | 43 ++++++- lib/route/calendar.js | 40 ++++-- package.json | 1 + t/integration/team_view/deducted_column.js | 44 +++---- .../team_view/deducted_column__visibility.js | 121 ++++++++++++++++++ views/team_view.hbs | 12 +- 6 files changed, 219 insertions(+), 42 deletions(-) create mode 100644 t/integration/team_view/deducted_column__visibility.js diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 552f09ef7..4109d3241 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -1,9 +1,11 @@ 'use strict'; -var moment = require('moment'), -Promise = require('bluebird'), -_ = require('underscore'); +const + moment = require('moment'), + Promise = require('bluebird'), + Joi = require('joi'), + _ = require('underscore'); function TeamView(args) { var me = this; @@ -87,7 +89,38 @@ TeamView.prototype.promise_team_view_details = function(args){ }; +// Experimenting with parameter validation done with Joi.js +const inject_statistics_args_schema = Joi.object() + .keys({ + leave_types : Joi.array().items( + Joi.object().keys({ + id : Joi.number().required(), + use_allowance : Joi.boolean().required(), + }) + ), + team_view_details : Joi.object().required().keys({ + users_and_leaves : Joi.array().required().items( + Joi.object().keys() + ) + }), + }); + +/* + * Takes "team view details" and enrich them with statistics about absences + * each employee has for given month + * + * */ + TeamView.prototype.inject_statistics = function(args){ + + // Validate parameters + let param_validation = Joi.validate(args, inject_statistics_args_schema, { allowUnknown : true }); + if (param_validation.error) { + console.log('An error occured when trying to validate args in inject_statistics.'); + console.dir(param_validation.error); + throw new Error('Failed to validate parameters in TeamView.inject_statistics'); + } + let team_view_details = args.team_view_details, leave_types = args.leave_types || []; @@ -137,4 +170,8 @@ TeamView.prototype.inject_statistics = function(args){ return Promise.resolve( team_view_details ); }; +TeamView.prototype.restrainStatisticsForUser = function(args){ + +}; + module.exports = TeamView; diff --git a/lib/route/calendar.js b/lib/route/calendar.js index 8b7f09fa5..8a55ec0bd 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -157,21 +157,35 @@ router.get('/teamview/', function(req, res){ department_id : current_deparment_id, }), req.user.get_company_with_all_leave_types(), - function(team_view_details, company){ - return team_view.inject_statistics({ - team_view_details : team_view_details, - leave_types : company.leave_types, - }) - .then(team_view_details => res.render('team_view', { - base_date : base_date, - prev_date : moment(base_date).add(-1,'month'), - next_date : moment(base_date).add(1,'month'), - users_and_leaves : team_view_details.users_and_leaves, - related_departments : team_view_details.related_departments, - current_department : team_view_details.current_department, - company : company, + (team_view_details, company) => { + // Enrich "team view details" with statistics as how many deducted days each employee spent current month + team_view + .inject_statistics({ + team_view_details : team_view_details, + leave_types : company.leave_types, }) + .then(team_view_details => res.render('team_view', { + base_date : base_date, + prev_date : moment(base_date).add(-1,'month'), + next_date : moment(base_date).add(1,'month'), + users_and_leaves : team_view_details.users_and_leaves, + related_departments : team_view_details.related_departments, + current_department : team_view_details.current_department, + company : company, + show_statistics : (req.user.supervised_users.length > 1), + }) + ); + }) + .catch(error => { + console.error( + 'An error occured when user '+req.user.id+ + ' tried to access Teamview page: '+error ); + req.session.flash_error('Failed to access Teamview page. Please contact administrator.'); + if (error.hasOwnProperty('user_message')) { + req.session.flash_error(error.user_message); + } + return res.redirect_with_session('/'); }); }); diff --git a/package.json b/package.json index e1378fb2b..221921717 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "formidable": "~1.0.17", "html-to-text": "^3.2.0", "ical-generator": "^0.2.7", + "joi": "~12.0.0", "ldapauth-fork": "^2.5.2", "moment": "^2.11.2", "moment-timezone": "^0.5.10", diff --git a/t/integration/team_view/deducted_column.js b/t/integration/team_view/deducted_column.js index a39b75d35..3d7f409ff 100644 --- a/t/integration/team_view/deducted_column.js +++ b/t/integration/team_view/deducted_column.js @@ -2,30 +2,7 @@ 'use strict'; /* - * Scenario 1: - * - * Check that values for new columns are shown only for employess - * currently login user can supervise. - * - * The reason for this sceanrio is because in UK for instance it is illegal to share - * details for employees who are not supervisers. Peers should not know how many days - * their coleagues were off sick for instance. - * - * * create account by admin user A - * * add user B - * * add user C - * * ensure company has "Share absences between all employees" flag ON - * * make user B to be superviser of user C - * * login as user A and ensure team view shows deducted values for all three users - * * login as user B and ensure she sees deducted days only for user B (self) and user C - * but not for user A - * * login as user C and ensure she sees only values for her account - * - * */ - - -/* - * Scenario 2: + * Scenario: * * Case when holidays spans through more then one month and is devided by bank holiday. * @@ -324,7 +301,7 @@ describe('Case when holidays spans through more then one month and is devided by .then(() => done()); }); - it('As user B cretae a holiday request from 24 Aug to 24 Aug 2016', function(done){ + it('As user B cretae a holiday request from 24 Aug to 24 Aug 2016 (but not approved)', function(done){ submit_form_func({ driver : driver, form_params : [{ @@ -339,6 +316,23 @@ describe('Case when holidays spans through more then one month and is devided by .then(function(){done()}); }); + it("Logout from user B", function(done){ + logout_user_func({ + application_host : application_host, + driver : driver, + }) + .then(() => done()); + }); + + it("Login as user A (admin)", function(done){ + login_user_func({ + application_host : application_host, + user_email : email_A, + driver : driver, + }) + .then(() => done()); + }); + it('Navigate to team view and ensure that it shows 9 days were deducted for Aug 2016', function(done){ open_page_func({ url : application_host + 'calendar/teamview/?date=2016-08', diff --git a/t/integration/team_view/deducted_column__visibility.js b/t/integration/team_view/deducted_column__visibility.js new file mode 100644 index 000000000..95916b21c --- /dev/null +++ b/t/integration/team_view/deducted_column__visibility.js @@ -0,0 +1,121 @@ + +'use strict'; + + +/* + * Scenario: + * + * Check that values for new columns are shown only for employess + * currently login user can supervise. + * + * The reason for this sceanrio is because in UK for instance it is illegal to share + * details for employees who are not supervisers. Peers should not know how many days + * their coleagues were off sick for instance. + * + * * create account by admin user A + * * add user B + * * add user C + * * ensure company has "Share absences between all employees" flag ON + * * make user B to be superviser of user C + * * login as user A and ensure team view shows deducted values for all three users + * * login as user B and ensure she sees deducted days + * * login as user C and ensure she does not see allowance deduction dates + * + * */ + + +const + test = require('selenium-webdriver/testing'), + By = require('selenium-webdriver').By, + Promise = require("bluebird"), + expect = require('chai').expect, + add_new_user_func = require('../../lib/add_new_user'), + check_elements_func = require('../../lib/check_elements'), + config = require('../../lib/config'), + login_user_func = require('../../lib/login_with_user'), + logout_user_func = require('../../lib/logout_user'), + open_page_func = require('../../lib/open_page'), + register_new_user_func = require('../../lib/register_new_user'), + submit_form_func = require('../../lib/submit_form'), + user_info_func = require('../../lib/user_info'), + application_host = config.get_application_host(); + +describe('Check that values for new columns are shown only for employess currently login user can supervise', function(){ + + this.timeout( config.get_execution_timeout() ); + + let driver, + email_A, user_id_A, + email_B, user_id_B, + email_C, user_id_C; + + it("Register new company as admin user A", function(done){ + register_new_user_func({ + application_host : application_host, + }) + .then(data => { + driver = data.driver; + email_A = data.email; + done(); + }); + }); + + it("Create second user B", function(done){ + add_new_user_func({ + application_host : application_host, + driver : driver, + }) + .then(data => { + email_B = data.new_user_email; + done(); + }); + }); + + it("Create second user C", function(done){ + add_new_user_func({ + application_host : application_host, + driver : driver, + }) + .then(data => { + email_C = data.new_user_email; + done(); + }); + }); + + it("Obtain information about user A", function(done){ + user_info_func({ + driver : driver, + email : email_A, + }) + .then(data => { + user_id_A = data.user.id; + done(); + }); + }); + + it("Obtain information about user B", function(done){ + user_info_func({ + driver : driver, + email : email_B, + }) + .then(data => { + user_id_B = data.user.id; + done(); + }); + }); + + it("Obtain information about user C", function(done){ + user_info_func({ + driver : driver, + email : email_C, + }) + .then(data => { + user_id_C = data.user.id; + done(); + }); + }); + + after(function(done){ + driver.quit().then(() => done()); + }); +}); diff --git a/views/team_view.hbs b/views/team_view.hbs index 442bcfff9..47610fd9f 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -47,7 +47,17 @@ {{# each users_and_leaves}} {{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}} - {{ statistics.deducted_days }} + + {{# if ../show_statistics }}{{ statistics.deducted_days }}{{/if}} + {{/each}} From 91f514bf7625d990d6cf6e69484142c7122d4ff5 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 14 Nov 2017 17:09:25 +0000 Subject: [PATCH 015/539] Restrict presentation of allowance usage sattistics Users can see allowance deduction statistic only for employees they can supervise. --- lib/model/team_view.js | 25 ++ lib/route/calendar.js | 5 +- .../team_view/deducted_column__visibility.js | 229 +++++++++++++++++- views/team_view.hbs | 4 +- 4 files changed, 257 insertions(+), 6 deletions(-) diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 4109d3241..e3318aa29 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -170,8 +170,33 @@ TeamView.prototype.inject_statistics = function(args){ return Promise.resolve( team_view_details ); }; +/* + * Take "team view details" and user for whom them were generated + * and ensure the details contain statisticts for only those employees + * current user has access to. + * + * */ + TeamView.prototype.restrainStatisticsForUser = function(args){ + // TODO Consider parameters validation in the same fashion as in inject_statistics method + + let + team_view_details = args.team_view_details, + observer_user = args.user; + + let supervised_user_map = {}; + observer_user.supervised_users.forEach( u => supervised_user_map[ u.id ] = u); + + team_view_details + .users_and_leaves + .forEach(item => { + if (item.statistics && ! supervised_user_map[ item.user.id ]) { + delete item.statistics; + } + }); + + return Promise.resolve( team_view_details ); }; module.exports = TeamView; diff --git a/lib/route/calendar.js b/lib/route/calendar.js index 8a55ec0bd..c67769e06 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -164,6 +164,10 @@ router.get('/teamview/', function(req, res){ team_view_details : team_view_details, leave_types : company.leave_types, }) + .then( team_view_details => team_view.restrainStatisticsForUser({ + team_view_details : team_view_details, + user : req.user, + })) .then(team_view_details => res.render('team_view', { base_date : base_date, prev_date : moment(base_date).add(-1,'month'), @@ -172,7 +176,6 @@ router.get('/teamview/', function(req, res){ related_departments : team_view_details.related_departments, current_department : team_view_details.current_department, company : company, - show_statistics : (req.user.supervised_users.length > 1), }) ); }) diff --git a/t/integration/team_view/deducted_column__visibility.js b/t/integration/team_view/deducted_column__visibility.js index 95916b21c..f1d26f452 100644 --- a/t/integration/team_view/deducted_column__visibility.js +++ b/t/integration/team_view/deducted_column__visibility.js @@ -18,8 +18,9 @@ * * ensure company has "Share absences between all employees" flag ON * * make user B to be superviser of user C * * login as user A and ensure team view shows deducted values for all three users - * * login as user B and ensure she sees deducted days - * * login as user C and ensure she does not see allowance deduction dates + * * login as user B and ensure she sees deducted days only for user B (self) and user C + * but not for user A + * * login as user C and ensure she sees only values for her account * * */ @@ -38,7 +39,9 @@ const register_new_user_func = require('../../lib/register_new_user'), submit_form_func = require('../../lib/submit_form'), user_info_func = require('../../lib/user_info'), - application_host = config.get_application_host(); + application_host = config.get_application_host(), + new_department_form_id = '#add_new_department_form', + company_edit_form_id ='#company_edit_form'; describe('Check that values for new columns are shown only for employess currently login user can supervise', function(){ @@ -115,6 +118,226 @@ describe('Check that values for new columns are shown only for employess current }); }); + it("Open page for editing company details", function(done){ + open_page_func({ + url : application_host + 'settings/general/', + driver : driver, + }) + .then(() => done()); + }); + + it('Ensure company has "Share absences between all employees" flag OFF', function( done ){ + check_elements_func({ + driver : driver, + elements_to_check : [{ + selector : company_edit_form_id+' input[name="share_all_absences"]', + value : 'off', + tick : true, + }], + }) + .then(() => done()); + }); + + it('... and tick that box ON', function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : company_edit_form_id+' input[name="share_all_absences"]', + tick : true, + value : 'on', + }], + submit_button_selector : company_edit_form_id+' button[type="submit"]', + message : /successfully/i, + should_be_successful : true, + }) + .then(() => done()); + }); + + it("Open department management page", function(done){ + open_page_func({ + url : application_host + 'settings/departments/', + driver : driver, + }) + .then(() => done()); + }); + + it("Add new department and make its approver to be user B", function(done){ + driver + .findElement(By.css('#add_new_department_btn')) + .then(el => el.click()) + .then(function(){ + + // This is very important line when working with Bootstrap modals! + driver.sleep(1000); + + submit_form_func({ + driver : driver, + form_params : [{ + selector : `${ new_department_form_id } input[name="name__new"]`, + // Just to make sure it is always first in the lists + value : 'AAAAA', + },{ + selector : `${ new_department_form_id } select[name="allowance__new"]`, + option_selector : 'option[value="15"]', + value : '15', + },{ + selector : `${ new_department_form_id } select[name="boss_id__new"]`, + option_selector : `select[name="boss_id__new"] option[value="${ user_id_B }"]`, + }], + submit_button_selector : `${ new_department_form_id } button[type="submit"]`, + message : /Changes to departments were saved/, + }) + .then(function(){ done() }); + }); + }); + + it("Open user editing page for user B", function(done){ + open_page_func({ + url : `${ application_host }users/edit/${ user_id_C }/`, + driver : driver, + }) + .then(() => done()); + }); + + it("And make sure it is part of the newly added department", function(done){ + submit_form_func({ + submit_button_selector : 'button#save_changes_btn', + driver : driver, + form_params : [{ + selector : 'select[name="department"]', + // Newly added department should be first in the list as it is + // sorted by AZ and department started with AA + option_selector : 'select[name="department"] option:nth-child(1)', + }], + message : /Details for .* were updated/, + }) + .then(function(){ done() }); + }); + + it('As user A ensure team view shows deducted values for all three users', function(done){ + open_page_func({ + url : `${ application_host }calendar/teamview/`, + driver : driver, + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql('0'); + return Promise.resolve(1); + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql('0'); + return Promise.resolve(1); + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql('0'); + return Promise.resolve(1); + }) + + .then(function(){ done() }); + }); + + it("Logout from user A (admin)", function(done){ + logout_user_func({ + application_host : application_host, + driver : driver, + }) + .then(() => done()); + }); + + it("Login as user B", function(done){ + login_user_func({ + application_host : application_host, + user_email : email_B, + driver : driver, + }) + .then(() => done()); + }); + + it('Login as user B and ensure she sees deducted days only for user B (self) and user C but not for user A', function(done){ + open_page_func({ + url : `${ application_host }calendar/teamview/`, + driver : driver, + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql(''); + return Promise.resolve(1); + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql('0'); + return Promise.resolve(1); + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql('0'); + return Promise.resolve(1); + }) + + .then(function(){ done() }); + }); + + it("Logout from user B", function(done){ + logout_user_func({ + application_host : application_host, + driver : driver, + }) + .then(() => done()); + }); + + it("Login as user C", function(done){ + login_user_func({ + application_host : application_host, + user_email : email_C, + driver : driver, + }) + .then(() => done()); + }); + + it('Login as user C and ensure she sees only values for her account', function(done){ + open_page_func({ + url : `${ application_host }calendar/teamview/`, + driver : driver, + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql(''); + return Promise.resolve(1); + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql(''); + return Promise.resolve(1); + }) + + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] td.teamview-deducted-days`))) + .then(el => el.getText()) + .then(txt => { + expect(txt).to.be.eql('0'); + return Promise.resolve(1); + }) + + .then(function(){ done() }); + }); + after(function(done){ driver.quit().then(() => done()); }); diff --git a/views/team_view.hbs b/views/team_view.hbs index 47610fd9f..315ce73d1 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -49,14 +49,14 @@ {{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}} - {{# if ../show_statistics }}{{ statistics.deducted_days }}{{/if}} + {{# if statistics }}{{ statistics.deducted_days }}{{/if}} {{/each}} From 5b772aefc8e4986e1fa415b0e65d4471da2a8aba Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 15 Nov 2017 16:27:44 +0000 Subject: [PATCH 016/539] Add minor typo in test. --- t/integration/department/one_by_one_crud.js | 1 + 1 file changed, 1 insertion(+) diff --git a/t/integration/department/one_by_one_crud.js b/t/integration/department/one_by_one_crud.js index eabdc012f..831d6d141 100644 --- a/t/integration/department/one_by_one_crud.js +++ b/t/integration/department/one_by_one_crud.js @@ -718,6 +718,7 @@ describe('CRUD for department secondary supervisers', function(){ }], }) .then(function(vals){ done() }); + }); }); after(function(done){ From 8cf9a17644f2e2e7c4caaa5064602d8ad4bf62e0 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 15 Nov 2017 17:29:21 +0000 Subject: [PATCH 017/539] Finish test for editing department's secondary supervisers --- t/integration/department/one_by_one_crud.js | 75 ++++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/t/integration/department/one_by_one_crud.js b/t/integration/department/one_by_one_crud.js index 831d6d141..12c610f41 100644 --- a/t/integration/department/one_by_one_crud.js +++ b/t/integration/department/one_by_one_crud.js @@ -33,7 +33,6 @@ var test = require('selenium-webdriver/testing'), * * */ -/* describe('Check departments list page', function(){ var driver; @@ -179,7 +178,6 @@ describe('Check departments list page', function(){ driver.quit().then(function(){ done(); }); }); }); -*/ /* * Scenario: @@ -197,7 +195,6 @@ describe('Check departments list page', function(){ * * */ -/* describe("Edit individual department via department details page", function(){ var driver, email_A, email_B, user_id_A, user_id_B, department_edit_page_url, new_department_id; @@ -491,7 +488,6 @@ describe("Edit individual department via department details page", function(){ driver.quit().then(function(){ done(); }); }); }); -*/ /* * Scenario (edditing secondary supervisers) @@ -505,7 +501,6 @@ describe("Edit individual department via department details page", function(){ * has only user B and C in the list * * tick user B and save changes * * observe that user B appeares on the list of secondary supervisers - * * * open "add supervisors" pop up again and ensure that user B has tick * next to it and user C does not have it * * tick user C and un-tick user B and save changes @@ -678,7 +673,7 @@ describe('CRUD for department secondary supervisers', function(){ submit_form_func({ driver : driver, form_params : [{ - selector : 'input[name="supervisor_id"][value="'+user_id_B+'"]', + selector : 'input[name="supervisor_id"][value="'+user_id_B+'"]', tick : true, value : 'on', }], @@ -686,7 +681,6 @@ describe('CRUD for department secondary supervisers', function(){ message : /Supervisors were added to department/, }) .then(function(){ done() }); - }); it('Observe that user B appeares on the list of secondary supervisers', function(done){ @@ -702,27 +696,74 @@ describe('CRUD for department secondary supervisers', function(){ }); }); - it('open "add supervisors" pop up again and ensure that user B has tick', function(done){ + it('Open "add supervisors" pop up again and ensure that user B has tick next to it and user C does not have it', function(done){ driver .findElement(By.css('a[data-vpp-add-new-secondary-supervisor="1"]')) - .then(function(btn){ return btn.click() }) - .then(function(){ - driver.sleep(1000); - - return check_elements_func({ + .then( btn => btn.click() ) + .then( () => driver.sleep(1000) ) + .then( () => check_elements_func({ driver : driver, elements_to_check : [{ - selector : 'input[name="supervisor_id"][value="'+user_id_B+'"]', + selector : `input[name="supervisor_id"][value="${ user_id_B }"]`, tick : true, value : 'on', }], }) - .then(function(vals){ done() }); - }); + ) + .then( () => check_elements_func({ + driver : driver, + elements_to_check : [{ + selector : `input[name="supervisor_id"][value="${ user_id_C }"]`, + tick : true, + value : 'off', + }], + }) + ) + .then(() => done()); + }); + + it('Tick user C and un-tick user B and save changes', function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : 'input[name="supervisor_id"][value="'+user_id_B+'"]', + tick : true, + },{ + selector : 'input[name="supervisor_id"][value="'+user_id_C+'"]', + tick : true, + }], + submit_button_selector : 'button[name="do_add_supervisors"]', + message : /Supervisors were added to department/, + }) + .then(() => done()); + }); + + it('Observe that "secondary spervisors" section now contains only user C', function(done) { + driver + .findElements(By.css('button[name="remove_supervisor_id"]')) + .then(els => { + expect(els.length).to.be.eql(1, 'No remove buttons for supervisers as there are not any'); + return els[0].getAttribute('value'); + }) + .then(val => { + expect(val).to.be.eql(String(user_id_C), 'It is indeed user C'); + done(); + }); + }); + + it('Click on "Remove" button next to user C and observe that it disappeares from "secondary supervisors" section after page is reloaded', function(done){ + driver + .findElement(By.css(`button[name="remove_supervisor_id"][value="${ user_id_C }"]`)) + .then(el => el.click()) + .then(() => driver.findElements(By.css('button[name="remove_supervisor_id"]'))) + .then(els => { + expect(els.length, 'There is no users in secondary supervisers section').to.be.eql(0); + done(); + }); }); after(function(done){ - driver.quit().then(function(){ done(); }); + driver.quit().then(() => done()); }); }); From d58232963b99a07059534bec1f12678705d38962 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 16 Nov 2017 14:57:54 +0000 Subject: [PATCH 018/539] Replace old get_superviser to be get_boss --- lib/model/db/leave.js | 6 +----- lib/model/db/user.js | 31 +++++++++++++++++++++---------- lib/route/calendar.js | 4 +++- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/model/db/leave.js b/lib/model/db/leave.js index 7aa19181d..1bc0811f1 100644 --- a/lib/model/db/leave.js +++ b/lib/model/db/leave.js @@ -126,11 +126,7 @@ module.exports = function(sequelize, DataTypes) { return Promise.try(function(){ return employee.validate_overlapping(valide_attributes); }) - .then(function(){ - // TODO replace it with simply reusing boss_id of department - return employee.promise_superviser(); - }) - + .then(() => employee.promise_boss()) .then(function(superviser){ var start_date = moment(valide_attributes.from_date), diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 0de4ff3c6..68d610291 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -184,18 +184,29 @@ function get_instance_methods(sequelize) { }, // promise_users_I_can_manage - promise_superviser : function(){ - return this.getDepartment({ - include : [{ - model : sequelize.models.User, - as : 'boss', - }] - }) - .then(function(department){ - return Promise.resolve( department.boss ); - }); + /* + * Return user's boss, the head of department user belongs to + * + * */ + promise_boss : function(){ + return this.getDepartment({ + scope : ['with_boss'], + }) + .then(department => Promise.resolve( department.boss )); }, + /* + * Return users who could supervise current user, that is those who could + * approve its leave requests and who can create leave requests on behalf of + * thos user. + * + * */ + promise_supervisers : function(){ + return this.getDepartment({ + scope : ['with_boss', 'with_supervisors'], + }) + .then( department => Promise.resolve( Array.concat( [department.boss], department.supervisers ) ) ); + }, calculate_number_of_days_available_in_allowance : function(year){ if (! year ){ diff --git a/lib/route/calendar.js b/lib/route/calendar.js index c9fa4ae97..cd72b9a4e 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -102,10 +102,12 @@ router.get('/', function(req, res) { }), req.user.get_company_with_all_leave_types(), req.user.reload_with_leave_details({ year : current_year }), - req.user.promise_superviser(), + req.user.promise_boss(), function(calendar, company, user, superviser){ var full_leave_type_statistics = user.get_leave_statistics_by_types(); + // TODO VPP: replace superviser with superviserS + res.render('calendar', { calendar : _.map(calendar, function(c){return c.as_for_template()}), company : company, From 3008893272ce5a26cec41a775206b7c989913fdb Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 16 Nov 2017 15:50:53 +0000 Subject: [PATCH 019/539] Record actual user who processed given leave request --- lib/model/db/leave.js | 52 +++++++++++++++++++++++++++++++++---------- lib/model/db/user.js | 2 +- lib/route/requests.js | 2 +- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/lib/model/db/leave.js b/lib/model/db/leave.js index 1bc0811f1..0fb0a7672 100644 --- a/lib/model/db/leave.js +++ b/lib/model/db/leave.js @@ -349,26 +349,56 @@ get_deducted_days : function(args) { }, // End get_deducted_days promise_to_reject : function() { + let self = this; + + if ( ! args ) { + args = {}; + } + + if ( ! args.by_user ) { + throw new Error('promise_to_reject has to have by_user parameter'); + } + + let by_user = args.by_user; + // See explanation to promise_to_approve - this.status = this.is_pended_revoke_leave() ? + self.status = self.is_pended_revoke_leave() ? Leave.status_approved(): Leave.status_rejected(); - return this.save(); + + self.approverId = by_user.id; + + return self.save(); }, -promise_to_approve : function() { +promise_to_approve : function(args) { + let self = this; + + if ( ! args ) { + args = {}; + } + + if ( ! args.by_user ) { + throw new Error('promise_to_approve has to have by_user parameter'); + } + + let by_user = args.by_user; + // If current leave is one with requested revoke, then // approve action set it into Rejected status // otherwise it is approve action for new leave // so put leave into Approved - this.status = this.is_pended_revoke_leave() ? + self.status = self.is_pended_revoke_leave() ? Leave.status_rejected(): Leave.status_approved(); - return this.save(); + + self.approverId = by_user.id; + + return self.save(); }, -promise_to_revoke : function(){ - var self = this; +promise_to_revoke : function(args){ + let self = this; return self.getUser({ include : [ @@ -384,15 +414,13 @@ promise_to_revoke : function(){ ? Leave.status_rejected() : Leave.status_pended_revoke(); - // TODO this needs to be populated based on actual user who performs - // the "revoke" action. - // In fact it needs to be passed as a parameter. - // And if it is an autoapprove case set it as department's boss + // By default it is user main boss is one who has to approve the revoked request self.approverId = user.department.bossId; + self.status = new_leave_status; return self.save(); - }) + }); }, promise_to_cancel : function(){ diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 68d610291..9919f0a95 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -198,7 +198,7 @@ function get_instance_methods(sequelize) { /* * Return users who could supervise current user, that is those who could * approve its leave requests and who can create leave requests on behalf of - * thos user. + * those user. * * */ promise_supervisers : function(){ diff --git a/lib/route/requests.js b/lib/route/requests.js index 390153b18..d8ba0b77d 100644 --- a/lib/route/requests.js +++ b/lib/route/requests.js @@ -62,7 +62,7 @@ function leave_request_action(args) { was_pended_revoke = leave_to_process.is_pended_revoke_leave(); - return leave_to_process[leave_action_method](); + return leave_to_process[leave_action_method]({ by_user : req.user }); }) .then(function(processed_leave){ return processed_leave.reload({ From 458ef549c0fe52d0bca3a7f0f9408dd9ff96e53a Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 16 Nov 2017 18:25:26 +0000 Subject: [PATCH 020/539] Replace supervised departments implementation --- lib/model/db/user.js | 41 +++++++++++++++++++++++++++-------------- lib/model/team_view.js | 2 +- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 9919f0a95..ec9cd334a 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -150,12 +150,7 @@ function get_instance_methods(sequelize) { // If current user has any departments under supervision then get // all users from those departments plus user himself, // if no supervised users an array with only current user is returned - return this_user.getSupervised_departments({ - include : [{ - model : sequelize.models.User, - as : 'users', - }], - }) + return this_user.promise_supervised_department() .then(function(departments){ var users = _.flatten( _.map( @@ -208,6 +203,29 @@ function get_instance_methods(sequelize) { .then( department => Promise.resolve( Array.concat( [department.boss], department.supervisers ) ) ); }, + promise_supervised_department : function() { + let self = this; + + return sequelize.models.DepartmentSupervisor.findAll({ where : { user_id : self.id } }) + // Obtain departments current user supervises as secondary superviser + .then(department_supervisers => department_supervisers.map( obj => obj.department_id )) + .then( department_ids => { + + if ( department_ids ) { + department_ids = []; + } + + return sequelize.models.Department.scope('with_simple_users').findAll({ + where : { + $or : [ + { id : department_ids }, + { bossId : self.id }, + ] + } + }); + }); + }, + calculate_number_of_days_available_in_allowance : function(year){ if (! year ){ year = moment().format('YYYY'); @@ -271,13 +289,13 @@ function get_instance_methods(sequelize) { } // make sure I am not supervisor, otherwise throw an error - return self.getSupervised_departments() - .then(function(departments){ + return self.promise_supervised_department() + .then(departments => { if (departments.length > 0){ throw new Error("Cannot remove supervisor"); } - return self.getMy_leaves() + return self.getMy_leaves(); }) .then(function(leaves){ // remove all leaves @@ -528,11 +546,6 @@ function withAssociations() { as : 'supervised_leaves', foreignKey : 'approverId', }); - models.User.hasMany(models.Department, { - as : 'supervised_departments', - foreignKey : 'bossId', - constraints: false, - }); models.User.hasMany(models.UserFeed, { as : 'feeds', foreignKey : 'userId', diff --git a/lib/model/team_view.js b/lib/model/team_view.js index e3318aa29..499cab60b 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -63,7 +63,7 @@ TeamView.prototype.promise_team_view_details = function(args){ // Promise departments either supervised by current user or one that she belongs to promise_departments = Promise.join( user.getDepartment(), - user.getSupervised_departments(), + user.promise_supervised_department(), normalise_departments_func ); } From 4615a760df6c48a2c057bc3719c53f57297786ad Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 17 Nov 2017 10:53:53 +0000 Subject: [PATCH 021/539] Allow secondary approvers to process incoming leaves. --- lib/model/db/leave.js | 2 +- lib/model/db/user.js | 21 ++++++-- lib/model/mixin/user/absence_aware.js | 69 +++++++++++++++------------ 3 files changed, 55 insertions(+), 37 deletions(-) diff --git a/lib/model/db/leave.js b/lib/model/db/leave.js index 0fb0a7672..cf8048f14 100644 --- a/lib/model/db/leave.js +++ b/lib/model/db/leave.js @@ -348,7 +348,7 @@ get_deducted_days : function(args) { return leave_days; }, // End get_deducted_days -promise_to_reject : function() { +promise_to_reject : function(args) { let self = this; if ( ! args ) { diff --git a/lib/model/db/user.js b/lib/model/db/user.js index ec9cd334a..79cc15bc3 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -135,6 +135,11 @@ function get_instance_methods(sequelize) { return this.name + ' ' + this.lastname; }, + // TODO VPP: rename this method as its name misleading: it returns all users + // managed by current users + user itslef, so it should be something likne + // "promise_all_supervised_users_plus_me" + // In fact this method probably have to be ditched in favour of more granular ones + // promise_users_I_can_manage : function(){ var this_user = this; @@ -211,7 +216,7 @@ function get_instance_methods(sequelize) { .then(department_supervisers => department_supervisers.map( obj => obj.department_id )) .then( department_ids => { - if ( department_ids ) { + if ( ! department_ids ) { department_ids = []; } @@ -226,6 +231,16 @@ function get_instance_methods(sequelize) { }); }, + promise_supervised_users : function () { + let self = this; + + return self + .promise_supervised_department() + .then(departments => { + return self.Model.findAll({ where : { DepartmentId : departments.map(d => d.id ) } }); + }) + }, + calculate_number_of_days_available_in_allowance : function(year){ if (! year ){ year = moment().format('YYYY'); @@ -542,10 +557,6 @@ function withAssociations() { as : 'my_leaves', foreignKey : 'userId', }); - models.User.hasMany(models.Leave, { - as : 'supervised_leaves', - foreignKey : 'approverId', - }); models.User.hasMany(models.UserFeed, { as : 'feeds', foreignKey : 'userId', diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index b8add69c5..5ba7419aa 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -279,40 +279,47 @@ module.exports = function(sequelize){ // Promise leaves that are needed to be Approved/Rejected // this.promise_leaves_to_be_processed = function(){ - return this.getSupervised_leaves({ - include : [{ - model : sequelize.models.LeaveType, - as : 'leave_type', - },{ - model : sequelize.models.User, - as : 'user', - include : [{ - model : sequelize.models.Company, - as : 'company', + let self = this; + + return self + .promise_supervised_users() + .then(users => { + return sequelize.models.Leave.findAll({ include : [{ - model : sequelize.models.BankHoliday, - as : 'bank_holidays', + model : sequelize.models.LeaveType, + as : 'leave_type', + },{ + model : sequelize.models.User, + as : 'user', + include : [{ + model : sequelize.models.Company, + as : 'company', + include : [{ + model : sequelize.models.BankHoliday, + as : 'bank_holidays', + }], + },{ + model : sequelize.models.Department, + as : 'department', + }], }], - },{ - model : sequelize.models.Department, - as : 'department', - }], - }], - where : { - status : [ - sequelize.models.Leave.status_new(), - sequelize.models.Leave.status_pended_revoke() - ] - }, - }) - .then(function(leaves){ - return Promise.resolve(leaves).map(function(leave){ - return leave.user.promise_schedule_I_obey(); - },{ - concurrency : 10, + where : { + status : [ + sequelize.models.Leave.status_new(), + sequelize.models.Leave.status_pended_revoke() + ], + userId : users.map(u => u.id), + }, + }) }) - .then(function(){ return Promise.resolve(leaves) }); - }); + .then( leaves => Promise + .resolve(leaves) + .map( + leave => leave.user.promise_schedule_I_obey(), + { concurrency : 10 } + ) + .then( () => Promise.resolve(leaves) ) + ); }; // END of promise_leaves_to_be_processed this.promise_cancelable_leaves = function(){ From 7d4cd6ac21488f2e83b50977fe90ba845e911a5e Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 17 Nov 2017 16:12:12 +0000 Subject: [PATCH 022/539] Some renamings --- lib/model/db/leave.js | 4 ++-- lib/model/db/user.js | 8 ++++---- lib/model/team_view.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/model/db/leave.js b/lib/model/db/leave.js index cf8048f14..ea3cc2a4a 100644 --- a/lib/model/db/leave.js +++ b/lib/model/db/leave.js @@ -127,7 +127,7 @@ module.exports = function(sequelize, DataTypes) { return employee.validate_overlapping(valide_attributes); }) .then(() => employee.promise_boss()) - .then(function(superviser){ + .then(function(main_superviser){ var start_date = moment(valide_attributes.from_date), end_date = moment(valide_attributes.to_date); @@ -147,7 +147,7 @@ module.exports = function(sequelize, DataTypes) { userId : employee.id, leaveTypeId : leave_type.id, status : new_leave_status, - approverId : superviser.id, + approverId : main_superviser.id, employee_comment : valide_attributes.reason, date_start : start_date.format('YYYY-MM-DD'), diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 79cc15bc3..8766b5e6c 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -155,7 +155,7 @@ function get_instance_methods(sequelize) { // If current user has any departments under supervision then get // all users from those departments plus user himself, // if no supervised users an array with only current user is returned - return this_user.promise_supervised_department() + return this_user.promise_supervised_departments() .then(function(departments){ var users = _.flatten( _.map( @@ -208,7 +208,7 @@ function get_instance_methods(sequelize) { .then( department => Promise.resolve( Array.concat( [department.boss], department.supervisers ) ) ); }, - promise_supervised_department : function() { + promise_supervised_departments : function() { let self = this; return sequelize.models.DepartmentSupervisor.findAll({ where : { user_id : self.id } }) @@ -235,7 +235,7 @@ function get_instance_methods(sequelize) { let self = this; return self - .promise_supervised_department() + .promise_supervised_departments() .then(departments => { return self.Model.findAll({ where : { DepartmentId : departments.map(d => d.id ) } }); }) @@ -304,7 +304,7 @@ function get_instance_methods(sequelize) { } // make sure I am not supervisor, otherwise throw an error - return self.promise_supervised_department() + return self.promise_supervised_departments() .then(departments => { if (departments.length > 0){ throw new Error("Cannot remove supervisor"); diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 499cab60b..53cbede9c 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -63,7 +63,7 @@ TeamView.prototype.promise_team_view_details = function(args){ // Promise departments either supervised by current user or one that she belongs to promise_departments = Promise.join( user.getDepartment(), - user.promise_supervised_department(), + user.promise_supervised_departments(), normalise_departments_func ); } From 5325c1662e927ef823a6fe275dcd22d2adeb5aa9 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 17 Nov 2017 16:29:03 +0000 Subject: [PATCH 023/539] More renamings - to eradicate supervisEr --- lib/model/db/company.js | 2 +- lib/model/db/leave.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/model/db/company.js b/lib/model/db/company.js index 34ecd86fd..404736fc5 100644 --- a/lib/model/db/company.js +++ b/lib/model/db/company.js @@ -263,7 +263,7 @@ module.exports = function(sequelize, DataTypes) { }); }) - // Update departments with correct user IDs for superviser + // Update departments with correct user IDs for supervisor // .then(function(){ return Promise.all( diff --git a/lib/model/db/leave.js b/lib/model/db/leave.js index ea3cc2a4a..bbcd55e50 100644 --- a/lib/model/db/leave.js +++ b/lib/model/db/leave.js @@ -127,7 +127,7 @@ module.exports = function(sequelize, DataTypes) { return employee.validate_overlapping(valide_attributes); }) .then(() => employee.promise_boss()) - .then(function(main_superviser){ + .then(function(main_supervisor){ var start_date = moment(valide_attributes.from_date), end_date = moment(valide_attributes.to_date); @@ -147,7 +147,7 @@ module.exports = function(sequelize, DataTypes) { userId : employee.id, leaveTypeId : leave_type.id, status : new_leave_status, - approverId : main_superviser.id, + approverId : main_supervisor.id, employee_comment : valide_attributes.reason, date_start : start_date.format('YYYY-MM-DD'), From 5ba47fed4a3fe408f264006a7954b5064af5591d Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 17 Nov 2017 16:36:31 +0000 Subject: [PATCH 024/539] More supervisers to become supervisors --- lib/model/db/user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 8766b5e6c..a4874fb65 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -201,11 +201,11 @@ function get_instance_methods(sequelize) { * those user. * * */ - promise_supervisers : function(){ + promise_supervisors : function(){ return this.getDepartment({ scope : ['with_boss', 'with_supervisors'], }) - .then( department => Promise.resolve( Array.concat( [department.boss], department.supervisers ) ) ); + .then( department => Promise.resolve( Array.concat( [department.boss], department.supervisors ) ) ); }, promise_supervised_departments : function() { From 180103ad19bd3c0ee8a83b9bb56210a09740802c Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Mon, 20 Nov 2017 16:57:28 +0000 Subject: [PATCH 025/539] Calendar to show all supervisors --- lib/model/db/user.js | 6 +++--- lib/route/calendar.js | 12 ++++-------- views/calendar.hbs | 7 ++++++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index a4874fb65..0e61581d4 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -205,15 +205,15 @@ function get_instance_methods(sequelize) { return this.getDepartment({ scope : ['with_boss', 'with_supervisors'], }) - .then( department => Promise.resolve( Array.concat( [department.boss], department.supervisors ) ) ); + .then( department => Promise.resolve( _.flatten([ department.boss, department.supervisors ]) ) ); }, promise_supervised_departments : function() { let self = this; return sequelize.models.DepartmentSupervisor.findAll({ where : { user_id : self.id } }) - // Obtain departments current user supervises as secondary superviser - .then(department_supervisers => department_supervisers.map( obj => obj.department_id )) + // Obtain departments current user supervises as secondary supervisor + .then(department_supervisors => department_supervisors.map( obj => obj.department_id )) .then( department_ids => { if ( ! department_ids ) { diff --git a/lib/route/calendar.js b/lib/route/calendar.js index cd72b9a4e..ffc6d3028 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -102,25 +102,21 @@ router.get('/', function(req, res) { }), req.user.get_company_with_all_leave_types(), req.user.reload_with_leave_details({ year : current_year }), - req.user.promise_boss(), - function(calendar, company, user, superviser){ + req.user.promise_supervisors(), + function(calendar, company, user, supervisors){ var full_leave_type_statistics = user.get_leave_statistics_by_types(); - // TODO VPP: replace superviser with superviserS - res.render('calendar', { calendar : _.map(calendar, function(c){return c.as_for_template()}), company : company, title : 'Calendar', current_user : user, - superviser : superviser, + supervisors : supervisors, previous_year : moment(current_year).add(-1,'year').format('YYYY'), current_year : current_year.format('YYYY'), next_year : moment(current_year).add(1,'year').format('YYYY'), show_full_year : show_full_year, - leave_type_statistics : _.filter(full_leave_type_statistics, function(st){ - return st.days_taken > 0; - }), + leave_type_statistics : _.filter(full_leave_type_statistics, st => st.days_taken > 0), full_leave_type_statistics : full_leave_type_statistics, this_year : moment().format('YYYY'), }); diff --git a/views/calendar.hbs b/views/calendar.hbs index 726eb3265..b7178b704 100644 --- a/views/calendar.hbs +++ b/views/calendar.hbs @@ -57,7 +57,12 @@
- +
    {{# each department.supervisors }} From 2948771aac580bb58c0cf3aeac366bb86aa11069 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 6 Dec 2017 12:47:53 +0000 Subject: [PATCH 028/539] $start migrating blocking functions to async style Migrate promise_number_of_days_available_in_allowance to non blocking version --- lib/model/db/user.js | 20 +++++++++++++ lib/model/mixin/user/absence_aware.js | 30 +++++++++++-------- package.json | 2 +- .../leave_request/basic_leave_request.js | 6 ++-- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 0e61581d4..39cfbd734 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -241,6 +241,13 @@ function get_instance_methods(sequelize) { }) }, + /* + * DEPRECATED + * + * Consider using non-blocking version: + * promise_number_of_days_available_in_allowance + * + * */ calculate_number_of_days_available_in_allowance : function(year){ if (! year ){ year = moment().format('YYYY'); @@ -249,6 +256,19 @@ function get_instance_methods(sequelize) { - this.calculate_number_of_days_taken_from_allowance({year : year}); }, + /* + * Async version of getting number of days remaining in allowance for + * current user + * + * */ + promise_number_of_days_available_in_allowance : function(year) { + let self = this; + + return Promise.resolve( + self.calculate_number_of_days_available_in_allowance(year) + ); + }, + reload_with_leave_details : function(args){ var year = args.year || moment(), diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index 5ba7419aa..027149fd6 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -525,24 +525,30 @@ module.exports = function(sequelize){ return employee.company.reload_with_bank_holidays() .then(function(){ return Promise.resolve(employee); }); }) - .then(function(employee){ + .then(employee => + employee.promise_number_of_days_available_in_allowance( + year.format('YYYY') + ) + .then(number_of_days => Promise.resolve([number_of_days, employee])) + ) + .then(function(args){ + let days_remaining_in_allowance = args[0], + employee = args[1]; - // Throw an exception when less than zero vacation would remain - // if we add currently requested absence - if ( - employee.calculate_number_of_days_available_in_allowance( - year.format('YYYY') - ) - - - leave.get_deducted_days_number({ + let deducted_days = leave.get_deducted_days_number({ year : year.format('YYYY'), user : employee, leave_type : leave_type, - }) - < - 0 + }); + + // Throw an exception when less than zero vacation would remain + // if we add currently requested absence + if ( + days_remaining_in_allowance - deducted_days < 0 ) { + console.log('Days remaining: ' + days_remaining_in_allowance + ', dayes deducted: ' + deducted_days); + var error = new Error('Requested absence is longer than remaining allowance'); error.user_message = error.toString(); throw error; diff --git a/package.json b/package.json index 221921717..03585aeb9 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "chai": "^2.2.0", - "mocha": "^2.2.4", + "mocha": "^3.5.3", "node-sass": "^4.5.3", "selenium-webdriver": "2.45.1" }, diff --git a/t/integration/leave_request/basic_leave_request.js b/t/integration/leave_request/basic_leave_request.js index 756ad7bf3..c25548c73 100644 --- a/t/integration/leave_request/basic_leave_request.js +++ b/t/integration/leave_request/basic_leave_request.js @@ -277,7 +277,7 @@ describe("Use problematic date with non default date format", function(){ it("Open calendar page", function(done){ open_page_func({ - url : application_host + 'calendar/?year=2017&show_full_year=1', + url : application_host + 'calendar/?year=2016&show_full_year=1', driver : driver, }) .then(function(){ done(); }) @@ -302,10 +302,10 @@ describe("Use problematic date with non default date format", function(){ value : "2", },{ selector : 'input#from', - value : '22/08/17', + value : '24/08/16', },{ selector : 'input#to', - value : '23/08/17', + value : '25/08/16', }], message : /New leave request was added/, }) From 9e250e521d89905079ea1ed01f9ac29761482b87 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 6 Dec 2017 14:03:31 +0000 Subject: [PATCH 029/539] Deprecate calculate_number_of_days_available_in_allowance --- lib/model/db/user.js | 9 ++++++-- lib/route/calendar.js | 39 ++++++++++++++++++------------- lib/route/users.js | 54 +++++++++++++++++++++++++++++++++++-------- views/calendar.hbs | 2 +- views/users.hbs | 6 +++-- 5 files changed, 80 insertions(+), 30 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 39cfbd734..8b208ba8d 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -248,7 +248,12 @@ function get_instance_methods(sequelize) { * promise_number_of_days_available_in_allowance * * */ - calculate_number_of_days_available_in_allowance : function(year){ + calculate_number_of_days_available_in_allowance : function(year, deprecate){ + + if ( deprecate !== 'deprecated' ) { + throw new Error('calculate_number_of_days_available_in_allowance is deprecated!'); + } + if (! year ){ year = moment().format('YYYY'); } @@ -265,7 +270,7 @@ function get_instance_methods(sequelize) { let self = this; return Promise.resolve( - self.calculate_number_of_days_available_in_allowance(year) + self.calculate_number_of_days_available_in_allowance(year, 'deprecated') ); }, diff --git a/lib/route/calendar.js b/lib/route/calendar.js index ffc6d3028..45a6eb291 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -104,22 +104,29 @@ router.get('/', function(req, res) { req.user.reload_with_leave_details({ year : current_year }), req.user.promise_supervisors(), function(calendar, company, user, supervisors){ - var full_leave_type_statistics = user.get_leave_statistics_by_types(); - - res.render('calendar', { - calendar : _.map(calendar, function(c){return c.as_for_template()}), - company : company, - title : 'Calendar', - current_user : user, - supervisors : supervisors, - previous_year : moment(current_year).add(-1,'year').format('YYYY'), - current_year : current_year.format('YYYY'), - next_year : moment(current_year).add(1,'year').format('YYYY'), - show_full_year : show_full_year, - leave_type_statistics : _.filter(full_leave_type_statistics, st => st.days_taken > 0), - full_leave_type_statistics : full_leave_type_statistics, - this_year : moment().format('YYYY'), - }); + let + full_leave_type_statistics = user.get_leave_statistics_by_types(), + this_year = moment().format('YYYY'); + + user + .promise_number_of_days_available_in_allowance( this_year ) + .then( number_of_days_available_in_allowance => res + .render('calendar', { + calendar : _.map(calendar, function(c){return c.as_for_template()}), + company : company, + title : 'Calendar', + current_user : user, + supervisors : supervisors, + previous_year : moment(current_year).add(-1,'year').format('YYYY'), + current_year : current_year.format('YYYY'), + next_year : moment(current_year).add(1,'year').format('YYYY'), + show_full_year : show_full_year, + leave_type_statistics : _.filter(full_leave_type_statistics, st => st.days_taken > 0), + full_leave_type_statistics : full_leave_type_statistics, + this_year : this_year, + number_of_days_available_in_allowance : number_of_days_available_in_allowance, + }) + ); } ); diff --git a/lib/route/users.js b/lib/route/users.js index 791a15697..f2d64c5b2 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -150,11 +150,18 @@ router.get('/edit/:user_id/absences/', function(req, res){ return employee.reload_with_session_details(); }) .then( employee => employee.reload_with_leave_details({})) - .then(employee => { + .then(employee => employee + .promise_number_of_days_available_in_allowance() + .then( days => Promise.resolve([days, employee]) ) + ) + .then(args => { + let + remaining_allowance = args[0], + employee = args[1]; let leave_statistics = { total_for_current_year : employee.calculate_total_number_of_days_n_allowance(), - remaining : employee.calculate_number_of_days_available_in_allowance(), + remaining : remaining_allowance, }; leave_statistics.used_so_far = leave_statistics.total_for_current_year - leave_statistics.remaining; @@ -601,13 +608,42 @@ router.get('/', function(req, res) { .then(function(){ return Promise.resolve(company) }); }) - .then(function(company){ - res.render('users', { - title : company.name + "'s people", - users : company.users, - company : company, - department_id : Number(department_id), - }); + /* + * Following block builds array of object for each user in company. + * Each object consist of following keys: + * - user_row : reference to the sequelize user row object + * - number_of_days_available_in_allowance : number of days remaining in allowance for given user + * + * This step is necessary because we are moving to non-blocking API for libraries, + * so we need to get all data before passing it into template as template + * + * */ + .then(company => Promise + .resolve(company.users) + .map(user => user + .promise_number_of_days_available_in_allowance() + .then(number_of_days => Promise.resolve({ + user_row : user, + number_of_days_available_in_allowance : number_of_days, + })), + { + concurrency : 10 + } + ) + .then(users_info => Promise.resolve([company, users_info])) + ) + + .then(function(args){ + let + company = args[0], + users_info = args[1]; + + res.render('users', { + company : company, + department_id : Number(department_id), + title : company.name + "'s people", + users_info : users_info, + }); }); }); diff --git a/views/calendar.hbs b/views/calendar.hbs index b7178b704..0fa2aad6b 100644 --- a/views/calendar.hbs +++ b/views/calendar.hbs @@ -22,7 +22,7 @@
    {{#with current_user }} -
    {{ this.calculate_number_of_days_available_in_allowance ../this_year }}
    +
    {{ ../number_of_days_available_in_allowance }}
    Days remaining
    out of {{this.calculate_total_number_of_days_n_allowance ../this_year}} in Allowance
    {{/with}} diff --git a/views/users.hbs b/views/users.hbs index 5031cc843..717f8d6c6 100644 --- a/views/users.hbs +++ b/views/users.hbs @@ -33,14 +33,16 @@ - {{# each users}} + {{# each users_info}} + {{# with this.user_row}} {{this.full_name}} {{this.department.name}} {{# if this.admin }}Yes{{else}}{{/if}} - {{ this.calculate_number_of_days_available_in_allowance}} + {{ ../number_of_days_available_in_allowance }} {{this.calculate_number_of_days_taken_from_allowance}} + {{/with}} {{/each}} From e0a93d185026aaea878eb35e2039e17e4b05aa07 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 6 Dec 2017 14:16:38 +0000 Subject: [PATCH 030/539] Completely remove promise_number_of_days_available_in_allowance --- lib/model/db/user.js | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 8b208ba8d..4686abbaa 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -242,39 +242,23 @@ function get_instance_methods(sequelize) { }, /* - * DEPRECATED - * - * Consider using non-blocking version: - * promise_number_of_days_available_in_allowance + * Async version of getting number of days remaining in allowance for + * current user * * */ - calculate_number_of_days_available_in_allowance : function(year, deprecate){ - - if ( deprecate !== 'deprecated' ) { - throw new Error('calculate_number_of_days_available_in_allowance is deprecated!'); - } + promise_number_of_days_available_in_allowance : function(year) { + let self = this; if (! year ){ year = moment().format('YYYY'); } - return this.calculate_total_number_of_days_n_allowance(year) - - this.calculate_number_of_days_taken_from_allowance({year : year}); - }, - /* - * Async version of getting number of days remaining in allowance for - * current user - * - * */ - promise_number_of_days_available_in_allowance : function(year) { - let self = this; + let number_of_days = self.calculate_total_number_of_days_n_allowance(year) + - self.calculate_number_of_days_taken_from_allowance({year : year}); - return Promise.resolve( - self.calculate_number_of_days_available_in_allowance(year, 'deprecated') - ); + return Promise.resolve( number_of_days ); }, - reload_with_leave_details : function(args){ var year = args.year || moment(), self = this; From 80073ee8a599ba01a0380f499f6b12eb214ed986 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Mon, 18 Dec 2017 12:50:53 +0000 Subject: [PATCH 031/539] Introduce little bit of caching --- lib/model/db/user.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 4686abbaa..d2b526971 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -242,20 +242,40 @@ function get_instance_methods(sequelize) { }, /* - * Async version of getting number of days remaining in allowance for - * current user + * Calculate number of days available in allowance for current employee. * * */ promise_number_of_days_available_in_allowance : function(year) { let self = this; - if (! year ){ + if ( ! year ){ year = moment().format('YYYY'); } + // To avoid redundant trips to DB check cached value first + if ( self.tom_cach && + self.tom_cach.hasOwnProperty('number_of_days_available_in_allowance') && + self.tom_cach.number_of_days_available_in_allowance.hasOwnProperty( year ) + ) { + return Promise.resolve( + self.tom_cach.number_of_days_available_in_allowance[year] + ); + } + let number_of_days = self.calculate_total_number_of_days_n_allowance(year) - self.calculate_number_of_days_taken_from_allowance({year : year}); + // Cach calculated value + if ( ! self.hasOwnProperty('tom_cach') ) { + self.tom_cach = {}; + } + + if ( ! self.tom_cach.hasOwnProperty('number_of_days_available_in_allowance')) { + self.tom_cach.number_of_days_available_in_allowance = {}; + } + + self.tom_cach.number_of_days_available_in_allowance[year] = number_of_days; + return Promise.resolve( number_of_days ); }, From a6068f763b8283b01962344d6860f13dfdba61bf Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Mon, 18 Dec 2017 16:23:21 +0000 Subject: [PATCH 032/539] Control GA tag over app config --- config/app.json | 1 + lib/view/helpers.js | 10 ++++++++++ views/layouts/main.hbs | 27 +++++++++++++-------------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/config/app.json b/config/app.json index 689890f2d..060c86fa8 100644 --- a/config/app.json +++ b/config/app.json @@ -10,6 +10,7 @@ "pass" : "pass" } }, + "ga_analytics_on" : false, "crypto_secret" : "!2~`HswpPPLa22+=±§sdq qwe,appp qwwokDF_", "application_domain" : "http://app.timeoff.management", "promotion_website_domain" : "http://timeoff.management" diff --git a/lib/view/helpers.js b/lib/view/helpers.js index c6b27d694..b8680b433 100644 --- a/lib/view/helpers.js +++ b/lib/view/helpers.js @@ -91,5 +91,15 @@ module.exports = function(){ return arg.join(''); }, + // Should we include Google Analitics snipet? + // (based on application config) + is_ga_analitics_on : function(options) { + if ( !!config.get('ga_analytics_on') ) { + return options.fn(this); + } + + return options.inverse(this); + }, + }; }; diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs index 500302aa6..3b3c6a8a9 100644 --- a/views/layouts/main.hbs +++ b/views/layouts/main.hbs @@ -29,21 +29,20 @@ - {{#each custom_java_script}} + {{~#each custom_java_script~}} - {{/each}} - + {{~/ is_ga_analitics_on ~}} - From 91b7927d575ed2c2d0bbe8b77ccca4fcb1bb3b85 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Mon, 18 Dec 2017 17:22:17 +0000 Subject: [PATCH 033/539] Introduce companywide mesasges --- lib/model/db/company.js | 6 ++++++ views/partials/header.hbs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/model/db/company.js b/lib/model/db/company.js index 404736fc5..209ca54c5 100644 --- a/lib/model/db/company.js +++ b/lib/model/db/company.js @@ -60,6 +60,12 @@ module.exports = function(sequelize, DataTypes) { allowNull : false, defaultValue : 'YYYY-MM-DD', }, + company_wide_message : { + type : DataTypes.TEXT, + allowNull : true, + defaultValue : null, + comment : 'Message shown to all users that belong to current company', + }, }, { indexes : [ diff --git a/views/partials/header.hbs b/views/partials/header.hbs index bba2e01b6..f7dce82ef 100644 --- a/views/partials/header.hbs +++ b/views/partials/header.hbs @@ -62,6 +62,12 @@
    +{{# if logged_user }} + {{# if logged_user.company.company_wide_message }} + + {{/if}} +{{/if}} +
From cc1be2d8e50913209a8552e8401960ee3242badd Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Mon, 18 Dec 2017 17:28:02 +0000 Subject: [PATCH 034/539] Add missing migration --- migrations/20171218-company-wide-message.js | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 migrations/20171218-company-wide-message.js diff --git a/migrations/20171218-company-wide-message.js b/migrations/20171218-company-wide-message.js new file mode 100644 index 000000000..99751b343 --- /dev/null +++ b/migrations/20171218-company-wide-message.js @@ -0,0 +1,27 @@ + +'use strict'; + +var models = require('../lib/model/db'); + +module.exports = { + up: function (queryInterface, Sequelize) { + + queryInterface.describeTable('Companies').then(function(attributes){ + + if (attributes.hasOwnProperty('company_wide_message')) { + return 1; + } + + return queryInterface.addColumn( + 'Companies', + 'company_wide_message', + models.Company.attributes.company_wide_message + ); + }); + + }, + + down: function (queryInterface, Sequelize) { + return queryInterface.removeColumn('Companies', 'company_wide_message'); + } +}; From 384964f118c457f4f05fdcc2a805dfb0dda85282 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 19 Dec 2017 15:15:47 +0000 Subject: [PATCH 035/539] Add table to track the allowance adjustment per year --- lib/model/db/user_allowance_adjustment.js | 43 +++++++++++++++++++ .../20171219-allowance-adjustment-per-year.js | 36 ++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 lib/model/db/user_allowance_adjustment.js create mode 100644 migrations/20171219-allowance-adjustment-per-year.js diff --git a/lib/model/db/user_allowance_adjustment.js b/lib/model/db/user_allowance_adjustment.js new file mode 100644 index 000000000..e4a471d2f --- /dev/null +++ b/lib/model/db/user_allowance_adjustment.js @@ -0,0 +1,43 @@ + +"use strict"; + +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes){ + let UserAllowanceAdjustment = sequelize.define("UserAllowanceAdjustment", { + year : { + type : DataTypes.INTEGER, + allowNull : false, + defaultValue : moment().format('YYYY'), + comment : 'Year when adjustment is applied', + }, + adjustment : { + type : DataTypes.INTEGER, + allowNull : false, + defaultValue : 0, + comment : 'Adjustment to allowance in current year', + }, + }, { + underscored : true, + freezeTableName : true, + timestamps : true, + createdAt : 'created_at', + updatedAt : false, + tableName : 'user_allowance_adjustment', + indexes : [{ + fields : [ 'user_id' ], + }], + + classMethods : { + associate : function(models) { + UserAllowanceAdjustment.belongsTo(models.User, { + as : 'user', + foreignKey : 'user_id', + }); + }, + }, + + }); + + return UserAllowanceAdjustment; +}; diff --git a/migrations/20171219-allowance-adjustment-per-year.js b/migrations/20171219-allowance-adjustment-per-year.js new file mode 100644 index 000000000..a8e54ee3d --- /dev/null +++ b/migrations/20171219-allowance-adjustment-per-year.js @@ -0,0 +1,36 @@ + +'use strict'; + +var models = require('../lib/model/db'), + Promise = require('bluebird'); + +module.exports = { + up: function (queryInterface, Sequelize) { + + queryInterface.describeTable('Users') + .then(function(attributes){ + + if ( ! attributes.hasOwnProperty('adjustment')) { + return 1; + } + + let sql = 'INSERT INTO user_allowance_adjustment (year, adjustment, user_id, created_at) ' + + 'SELECT 2017 AS year, adjustment as adjustment, id as user_id, date() || \' \' || time() as created_at ' + + 'FROM users'; + + console.log(sql); + + return queryInterface.sequelize.query( sql ); + }) + + // TODO remove the users.adjustment column + + .then(() => Promise.resolve()); + + }, + + down: function (queryInterface, Sequelize) { + // No way back! + return Promise.resolve(); + } +}; From ef001b3d7f67118c3c465883f0210f17104920a5 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 21 Dec 2017 09:31:29 +0000 Subject: [PATCH 036/539] Conver one more allawance calculating routine to be async --- lib/model/db/user.js | 30 +++++++++++++++++---------- lib/model/mixin/user/absence_aware.js | 25 +++++++++++++++++++++- lib/route/calendar.js | 8 ++++--- lib/route/users.js | 18 +++++++++++----- views/calendar.hbs | 4 ++-- 5 files changed, 63 insertions(+), 22 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index d2b526971..5bb756358 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -262,21 +262,29 @@ function get_instance_methods(sequelize) { ); } - let number_of_days = self.calculate_total_number_of_days_n_allowance(year) - - self.calculate_number_of_days_taken_from_allowance({year : year}); - // Cach calculated value - if ( ! self.hasOwnProperty('tom_cach') ) { - self.tom_cach = {}; - } - if ( ! self.tom_cach.hasOwnProperty('number_of_days_available_in_allowance')) { - self.tom_cach.number_of_days_available_in_allowance = {}; - } + return self - self.tom_cach.number_of_days_available_in_allowance[year] = number_of_days; + .promise_total_number_of_days_in_allowance(year) - return Promise.resolve( number_of_days ); + .then(total_days_number => total_days_number - self.calculate_number_of_days_taken_from_allowance({year : year})) + + .then(number_of_days => { + + // Cach calculated value + if ( ! self.hasOwnProperty('tom_cach') ) { + self.tom_cach = {}; + } + + if ( ! self.tom_cach.hasOwnProperty('number_of_days_available_in_allowance')) { + self.tom_cach.number_of_days_available_in_allowance = {}; + } + + self.tom_cach.number_of_days_available_in_allowance[year] = number_of_days; + + return Promise.resolve( number_of_days ); + }); }, reload_with_leave_details : function(args){ diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index 027149fd6..d2df14570 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -453,7 +453,7 @@ module.exports = function(sequelize){ }; - this.calculate_total_number_of_days_n_allowance = function(year) { + this._calculate_total_number_of_days_n_allowance = function(year) { // If optional paramater year was provided we need to calculate allowance // for that year, and if it is something other then current year, @@ -471,6 +471,29 @@ module.exports = function(sequelize){ + this.adjustment; }; + this.promise_total_number_of_days_in_allowance = function(year){ + let self = this; + + return Promise + + // First ensure that all necessary properties are available + .try(() => { + if ( ! self.department) { + return self.getDepartment() + .then(department => { + self.department = department; + return Promise.resolve(); + }) + } + + return Promise.resolve(); + }) + + .then(() => Promise.resolve( + self._calculate_total_number_of_days_n_allowance(year) + )); + }; + this.promise_my_leaves_for_calendar = function(args){ var year = args.year || moment(); diff --git a/lib/route/calendar.js b/lib/route/calendar.js index 45a6eb291..cb077901b 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -103,7 +103,8 @@ router.get('/', function(req, res) { req.user.get_company_with_all_leave_types(), req.user.reload_with_leave_details({ year : current_year }), req.user.promise_supervisors(), - function(calendar, company, user, supervisors){ + req.user.promise_total_number_of_days_in_allowance(current_year), + function(calendar, company, user, supervisors, total_number_of_days_in_allowance){ let full_leave_type_statistics = user.get_leave_statistics_by_types(), this_year = moment().format('YYYY'); @@ -121,10 +122,11 @@ router.get('/', function(req, res) { current_year : current_year.format('YYYY'), next_year : moment(current_year).add(1,'year').format('YYYY'), show_full_year : show_full_year, - leave_type_statistics : _.filter(full_leave_type_statistics, st => st.days_taken > 0), + leave_type_statistics : _.filter(full_leave_type_statistics, st => st.days_taken > 0), full_leave_type_statistics : full_leave_type_statistics, - this_year : this_year, + this_year : this_year, number_of_days_available_in_allowance : number_of_days_available_in_allowance, + total_number_of_days_in_allowance : total_number_of_days_in_allowance, }) ); } diff --git a/lib/route/users.js b/lib/route/users.js index f2d64c5b2..a978b5fd2 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -150,17 +150,25 @@ router.get('/edit/:user_id/absences/', function(req, res){ return employee.reload_with_session_details(); }) .then( employee => employee.reload_with_leave_details({})) - .then(employee => employee - .promise_number_of_days_available_in_allowance() - .then( days => Promise.resolve([days, employee]) ) + .then(employee => Promise.join( + employee + .promise_number_of_days_available_in_allowance() + .then( days => Promise.resolve([days, employee]) ), + employee + .promise_total_number_of_days_in_allowance(), + (args, total_days_number) => { + args.push(total_days_number); + return Promise.resolve(args); + }) ) .then(args => { let remaining_allowance = args[0], - employee = args[1]; + employee = args[1], + total_days_number = args[2]; let leave_statistics = { - total_for_current_year : employee.calculate_total_number_of_days_n_allowance(), + total_for_current_year : total_days_number, remaining : remaining_allowance, }; diff --git a/views/calendar.hbs b/views/calendar.hbs index 0fa2aad6b..4de7f4cf9 100644 --- a/views/calendar.hbs +++ b/views/calendar.hbs @@ -24,7 +24,7 @@ {{#with current_user }}
{{ ../number_of_days_available_in_allowance }}
Days remaining
-
out of {{this.calculate_total_number_of_days_n_allowance ../this_year}} in Allowance
+
out of {{ ../total_number_of_days_in_allowance }} in Allowance
{{/with}}
@@ -64,7 +64,7 @@ {{~/ each ~}}
Department: {{ current_user.department.name }}
-
Allowance in {{this_year}}: {{#with current_user}}{{ this.calculate_total_number_of_days_n_allowance ../this_year }}{{/with}} days
+
Allowance in {{this_year}}: {{ total_number_of_days_in_allowance }} days
From c2e3e142a6e75c3c24d1767ccb92f0eea3af4a80 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 21 Dec 2017 12:09:53 +0000 Subject: [PATCH 037/539] Use adjustemtn from source --- lib/model/db/user.js | 4 ++ lib/model/db/user_allowance_adjustment.js | 3 +- lib/model/mixin/user/absence_aware.js | 78 ++++++++++++++++++++--- lib/route/users.js | 60 +++++++++++++---- views/partials/user_details/absences.hbs | 2 +- views/partials/user_details/general.hbs | 2 - 6 files changed, 123 insertions(+), 26 deletions(-) diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 5bb756358..65a5c7bcb 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -598,6 +598,10 @@ function withAssociations() { as : 'feeds', foreignKey : 'userId', }); + models.User.hasMany(models.UserAllowanceAdjustment, { + as : 'adjustments', + foreignKey : 'user_id', + }); }; } diff --git a/lib/model/db/user_allowance_adjustment.js b/lib/model/db/user_allowance_adjustment.js index e4a471d2f..8e58f3d82 100644 --- a/lib/model/db/user_allowance_adjustment.js +++ b/lib/model/db/user_allowance_adjustment.js @@ -25,7 +25,8 @@ module.exports = function(sequelize, DataTypes){ updatedAt : false, tableName : 'user_allowance_adjustment', indexes : [{ - fields : [ 'user_id' ], + fields : [ 'user_id', 'year' ], + unique : true, }], classMethods : { diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index d2df14570..48dd8d940 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -452,23 +452,25 @@ module.exports = function(sequelize){ )); }; - - this._calculate_total_number_of_days_n_allowance = function(year) { + this._calculate_total_number_of_days_n_allowance = function(args) { + let + self = this, + year = args.year, + adjustment = args.adjustment; // If optional paramater year was provided we need to calculate allowance // for that year, and if it is something other then current year, // we use department nominal allowance plus employee's allowance // (ignoreing automatic allowance) if (year && year != moment().year()) { - return this.department.allowance - + this.adjustment; + return self.department.allowance + + adjustment; } // Get general allowance based on department - return this.department.allowance - + this.get_automatic_adjustment() - // Adjust it based on current user - + this.adjustment; + return self.department.allowance + + self.get_automatic_adjustment() + + adjustment; }; this.promise_total_number_of_days_in_allowance = function(year){ @@ -489,11 +491,67 @@ module.exports = function(sequelize){ return Promise.resolve(); }) - .then(() => Promise.resolve( - self._calculate_total_number_of_days_n_allowance(year) + // Fetch Adjustment for current year + .then(() => self.promise_adjustmet_for_year(year)) + + .then(adjustment => Promise.resolve( + self._calculate_total_number_of_days_n_allowance({ + year : year, + adjustment : adjustment, + }) )); }; + this.promise_adjustmet_for_year = function(year){ + let self = this; + + year = year || moment(); + year = moment(year).format('YYYY'); + + return self + .getAdjustments({ + where : { year : year } + }) + .then(adjustment_records => { + + // By deafault there is not adjustments + let adjustment = 0; + + if (adjustment_records.length === 1) { + adjustment = adjustment_records[0].adjustment; + } + + return Promise.resolve(adjustment); + }); + }; + + this.promise_to_update_adjustment = function(args) { + let + self = this, + year = args.year || moment().format('YYYY'), + adjustment = args.adjustment; + + // Update related allowance adjustement record + return sequelize.models.UserAllowanceAdjustment + .findOrCreate({ + where : { + user_id : self.id, + year : year, + }, + defaults : { adjustment : adjustment }, + }) + .spread((record, created) => { + + if ( created ) { + return Promise.resolve(); + } + + record.set('adjustment', adjustment); + + return record.save(); + }); + }; + this.promise_my_leaves_for_calendar = function(args){ var year = args.year || moment(); diff --git a/lib/route/users.js b/lib/route/users.js index a978b5fd2..cc4335ebf 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -67,8 +67,12 @@ router.post('/add/', function(req, res){ }); }) - .then(function(new_user_attributes){ - return model.User.create(new_user_attributes); + .then(new_user_attributes => { + + // User table is not going to hold adjustments + delete new_user_attributes.adjustment; + + return model.User.create(new_user_attributes) }) .then(function(new_user){ @@ -151,13 +155,20 @@ router.get('/edit/:user_id/absences/', function(req, res){ }) .then( employee => employee.reload_with_leave_details({})) .then(employee => Promise.join( + employee .promise_number_of_days_available_in_allowance() .then( days => Promise.resolve([days, employee]) ), + employee .promise_total_number_of_days_in_allowance(), - (args, total_days_number) => { + + employee + .promise_adjustmet_for_year(moment().format('YYYY')), + + (args, total_days_number, employee_adjustment) => { args.push(total_days_number); + args.push(employee_adjustment); return Promise.resolve(args); }) ) @@ -165,7 +176,8 @@ router.get('/edit/:user_id/absences/', function(req, res){ let remaining_allowance = args[0], employee = args[1], - total_days_number = args[2]; + total_days_number = args[2], + employee_adjustment = args[3]; let leave_statistics = { total_for_current_year : total_days_number, @@ -194,6 +206,7 @@ router.get('/edit/:user_id/absences/', function(req, res){ show_absence_tab : true, leave_type_statistics : employee.get_leave_statistics_by_types(), leave_statistics : leave_statistics, + employee_adjustment : employee_adjustment, }); }); }); @@ -408,12 +421,32 @@ router.post('/edit/:user_id/', function(req, res){ // All validations are passed: update database .then(function(){ - employee.updateAttributes(new_user_attributes).then(function(){ - req.session.flash_message( - 'Details for '+employee.full_name()+' were updated' - ); - return res.redirect_with_session(req.body.back_to_absences ? './absences/' : '.'); - }); + let adjustment = new_user_attributes.adjustment; + delete new_user_attributes.adjustment; + + employee + + // Update user record + .updateAttributes(new_user_attributes) + + // Update adjustment if necessary + .then(() => { + if ( adjustment !== undefined ) { + return employee.promise_to_update_adjustment({ + year : moment().format('YYYY'), + adjustment : adjustment, + }); + } + + return Promise.resolve(); + }) + + .then(function(){ + req.session.flash_message( + 'Details for '+employee.full_name()+' were updated' + ); + return res.redirect_with_session(req.body.back_to_absences ? './absences/' : '.'); + }); }) .catch(function(error){ @@ -667,7 +700,7 @@ function get_and_validate_user_parameters(args) { department_id = validator.trim(req.param('department')), start_date = validator.trim(req.param('start_date')), end_date = validator.trim(req.param('end_date')), - adjustment = validator.trim(req.param('adjustment')) || 0, + adjustment = validator.trim(req.param('adjustment')), password = validator.trim(req.param('password_one')), password_confirm = validator.trim(req.param('password_confirm')), admin = validator.toBoolean(req.param('admin')), @@ -748,11 +781,14 @@ function get_and_validate_user_parameters(args) { DepartmentId : department_id, start_date : start_date, end_date : (end_date || null), - adjustment : adjustment, admin : admin, auto_approve : auto_approve, }; + if (adjustment || String(adjustment) === '0') { + attributes.adjustment = adjustment; + } + if ( password ) { attributes.password = password; } diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index 1acecb81a..7519e5d44 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -61,7 +61,7 @@
- + working days
diff --git a/views/partials/user_details/general.hbs b/views/partials/user_details/general.hbs index a0d99c276..d448f4fd8 100644 --- a/views/partials/user_details/general.hbs +++ b/views/partials/user_details/general.hbs @@ -3,8 +3,6 @@
- - {{> user_details/breadcrumb employee=employee }}
From 84077db0e5ae5d19a530a632791a0a94585b08f7 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 21 Dec 2017 14:41:05 +0000 Subject: [PATCH 038/539] Remove user's adjustment column --- lib/model/db/user.js | 6 --- .../20171219-allowance-adjustment-per-year.js | 13 ++--- ...171220-drop-adjustment-column-from-user.js | 54 +++++++++++++++++++ 3 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 migrations/20171220-drop-adjustment-column-from-user.js diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 65a5c7bcb..8240184c3 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -73,12 +73,6 @@ module.exports = function(sequelize, DataTypes) { defaultValue : null, comment : 'Date emplyee stop working for company', }, - adjustment : { - type : DataTypes.INTEGER, - allowNull : false, - defaultValue : 0, - comment : 'Adjustment to allowance in current year', - }, }, { indexes : [ { diff --git a/migrations/20171219-allowance-adjustment-per-year.js b/migrations/20171219-allowance-adjustment-per-year.js index a8e54ee3d..19ef8e023 100644 --- a/migrations/20171219-allowance-adjustment-per-year.js +++ b/migrations/20171219-allowance-adjustment-per-year.js @@ -7,24 +7,25 @@ var models = require('../lib/model/db'), module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.describeTable('Users') + return queryInterface + .createTable( + models.UserAllowanceAdjustment.tableName, + models.UserAllowanceAdjustment.attributes + ) + .then(() => queryInterface.describeTable('Users')) .then(function(attributes){ if ( ! attributes.hasOwnProperty('adjustment')) { - return 1; + return Promise.resolve(); } let sql = 'INSERT INTO user_allowance_adjustment (year, adjustment, user_id, created_at) ' + 'SELECT 2017 AS year, adjustment as adjustment, id as user_id, date() || \' \' || time() as created_at ' + 'FROM users'; - console.log(sql); - return queryInterface.sequelize.query( sql ); }) - // TODO remove the users.adjustment column - .then(() => Promise.resolve()); }, diff --git a/migrations/20171220-drop-adjustment-column-from-user.js b/migrations/20171220-drop-adjustment-column-from-user.js new file mode 100644 index 000000000..4385d3368 --- /dev/null +++ b/migrations/20171220-drop-adjustment-column-from-user.js @@ -0,0 +1,54 @@ + +'use strict'; + +var models = require('../lib/model/db'), + Promise = require('bluebird'); + +module.exports = { + up: function (queryInterface, Sequelize) { + + return queryInterface + .describeTable('Users') + .then(attributes => { + + if ( ! attributes.hasOwnProperty('adjustment')) { + return Promise.resolve(); + } + + if ('sqlite' !== queryInterface.sequelize.getDialect()) { + // For non SQLite: it is easy + return queryInterface.removeColumn( + models.User.tableName, + 'adjustment' + ); + } + + // For SQLite it is "fun" + + return queryInterface + // Create Temp Users based on current model definitiom + .createTable('Users_backup', models.User.attributes) + + .then(function(){ + return queryInterface.sequelize.query('PRAGMA foreign_keys=off;'); + }) + + // Copy data form original Users into new Temp one + .then(function(){ + return queryInterface.sequelize.query( + 'INSERT INTO `Users_backup` (`id`, `email`, `password`, `name`, `lastname`, `activated`, `admin`, `start_date`, `end_date`, `createdAt`, `updatedAt`, `companyId`, `DepartmentId`, `auto_approve`) SELECT `id`, `email`, `password`, `name`, `lastname`, `activated`, `admin`, `start_date`, `end_date`, `createdAt`, `updatedAt`, `companyId`, `DepartmentId`, `auto_approve` FROM `'+ models.User.tableName +'`'); + }) + + .then(() => queryInterface.dropTable( models.User.tableName )) + .then(() => queryInterface.renameTable('Users_backup', models.User.tableName)) + .then(() => queryInterface.sequelize.query('PRAGMA foreign_keys=on;')) + .then(() => queryInterface.addIndex(models.User.tableName, ['companyId'])) + + }); + }, + + down: function (queryInterface, Sequelize) { + // No way back! + return Promise.resolve(); + } +}; From c3269a0d7a4bca01d92044daeeab73cd2b83dba4 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 21 Dec 2017 16:04:20 +0000 Subject: [PATCH 039/539] Add scrip used when checking real data on different app versions --- bin/fetch_user_stat.js | 107 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 bin/fetch_user_stat.js diff --git a/bin/fetch_user_stat.js b/bin/fetch_user_stat.js new file mode 100644 index 000000000..25d2d4c9c --- /dev/null +++ b/bin/fetch_user_stat.js @@ -0,0 +1,107 @@ + +'use strict'; + +var test = require('selenium-webdriver/testing'), + By = require('selenium-webdriver').By, + expect = require('chai').expect, + _ = require('underscore'), + Promise = require("bluebird"), + until = require('selenium-webdriver').until, + register_new_user_func = require('../t/lib/register_new_user'), + login_user_func = require('../t/lib/login_with_user'), + logout_user_func = require('../t/lib/logout_user'), + open_page_func = require('../t/lib/open_page'), + config = require('../t/lib/config'), + application_host = config.get_application_host(); + +/* + * THis is simple scrip to execute on different versions of application + * to check if new version introduces any anomalies. + * + * Example of running: + * + * node node_modules/mocha/bin/mocha --recursive bin/fetch_user_stat.js + * + * */ + +describe('Collect remaining days for employees', function(){ + + this.timeout( config.get_execution_timeout() ); + + var report = {}, + driver; + + it("Create new company", function(done){ + register_new_user_func({ + application_host : application_host, + }) + .then(function(data){ + driver = data.driver; + done(); + }); + }); + + it("Logout", function(done){ + logout_user_func({ + application_host : application_host, + driver : driver, + }) + .then(function(){ done() }); + }); + + // This is a list of accountes to iterate through + // By default it is dummy ones + [ + 'test@test.com', + 'test2@test.com' + ].forEach( email => { + + it("Login as user", function(done){ + login_user_func({ + application_host : application_host, + user_email : email, + driver : driver, + }) + .then(function(){ done() }); + }); + + it("Open users page", function(done){ + open_page_func({ + url : application_host + 'users/', + driver : driver, + }) + .then(function(){ done() }); + }); + + it("Fetch remaining days for each employee", function(done){ + + driver + .findElements(By.css('tr[data-vpp-user-row]')) + + .then(els => Promise.map(els, el => { + let user_id; + + return el.getAttribute('data-vpp-user-row') + .then(u_id => Promise.resolve( user_id = u_id )) + .then(() => el.findElement(By.css('td.vpp-days-remaining'))) + .then(el => el.getText()) + .then(days => Promise.resolve( report[ user_id ] = days )); + }, { concurrency : 0 })) + + .then(() => done()) + }); + + it("Logout", function(done){ + logout_user_func({ + application_host : application_host, + driver : driver, + }) + .then(function(){ done() }); + }); + }); + + after(function(done){ + console.dir(report); + driver.quit().then(function(){ done(); }); + }); +}); From 5bc9112d983c552b1b456ef512df4362023662d8 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 21 Dec 2017 16:14:51 +0000 Subject: [PATCH 040/539] Update UI. With more information about new logic behind allowance adjustement --- views/partials/user_details/absences.hbs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index 7519e5d44..ff63c9b99 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -59,14 +59,15 @@
- +
working days
-
Tune allowance for this user
+
Tune allowance for this user in current year.
Could be negative as well.
+
The value is valid during current year. Next year it needs to be re-confirmed.
From 06859cda5b66eeca5c85fd83d4b7698ad83537df Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 21 Dec 2017 18:58:51 +0000 Subject: [PATCH 041/539] Tweak the ico link Even though there is no such fil in codebase --- views/layouts/main.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs index 3b3c6a8a9..9b7380458 100644 --- a/views/layouts/main.hbs +++ b/views/layouts/main.hbs @@ -6,7 +6,7 @@ - + {{title}} From 6a6f86e78952ae878182e11b2a809836dc678040 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 22 Dec 2017 09:26:25 +0000 Subject: [PATCH 042/539] Sort out default ordering of leaves --- lib/model/mixin/user/absence_aware.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index 48dd8d940..6ae432637 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -245,6 +245,8 @@ module.exports = function(sequelize){ ); }) + .then(leaves => promise_to_sort_leaves(leaves)); + return promise_my_leaves; }; @@ -259,7 +261,8 @@ module.exports = function(sequelize){ sequelize.models.Leave.status_new(), sequelize.models.Leave.status_pended_revoke(), ], - }); + }) + .then(leaves => promise_to_sort_leaves(leaves)); }; // Promises leaves ever booked for current user @@ -272,7 +275,8 @@ module.exports = function(sequelize){ sequelize.models.Leave.status_new(), sequelize.models.Leave.status_pended_revoke(), ], - }); + }) + .then(leaves => promise_to_sort_leaves(leaves)); }; @@ -319,6 +323,7 @@ module.exports = function(sequelize){ { concurrency : 10 } ) .then( () => Promise.resolve(leaves) ) + .then(leaves => promise_to_sort_leaves(leaves)) ); }; // END of promise_leaves_to_be_processed @@ -335,7 +340,8 @@ module.exports = function(sequelize){ },{ concurrency : 10, }) - .then(function(){ return Promise.resolve(leaves) }); + .then(function(){ return Promise.resolve(leaves) }) + .then(leaves => promise_to_sort_leaves(leaves)); }); }; @@ -680,3 +686,15 @@ module.exports = function(sequelize){ }; +/* + * Simple function that sorts array of Leave objects in default way. + * + * */ + +function promise_to_sort_leaves(leaves) { + return Promise.resolve( leaves.sort( + (a,b) => a.date_start > b.date_start + ? -1 : a.date_start < b.date_start + ? 1 : 0 + )); +} From 64e549fa51e53fc29982f79d1106af210716f593 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 22 Dec 2017 12:03:05 +0000 Subject: [PATCH 043/539] Group leaves by year in lists --- lib/model/leave_collection.js | 67 ++++++++++++++++++++++++ lib/model/mixin/user/absence_aware.js | 31 ++++------- lib/route/requests.js | 11 ++-- lib/route/users.js | 12 +++-- views/calendar.hbs | 7 ++- views/partials/user_details/absences.hbs | 3 +- views/partials/user_requests.hbs | 6 --- views/partials/user_requests_grouped.hbs | 7 +++ views/requests.hbs | 9 +++- 9 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 lib/model/leave_collection.js create mode 100644 views/partials/user_requests_grouped.hbs diff --git a/lib/model/leave_collection.js b/lib/model/leave_collection.js new file mode 100644 index 000000000..055e9e7fe --- /dev/null +++ b/lib/model/leave_collection.js @@ -0,0 +1,67 @@ + +"use strict"; + +var + Promise = require('bluebird'), + moment = require('moment'), + _ = require('underscore'), + config = require('../config'); + +function promise_to_group_leaves(leaves) { + + if ( ! leaves ) { + throw new Error('Did not get "leaves" in promise_to_group_leaves'); + } + + let grouped_leaves = {}; + + // Group leaves by years + leaves.forEach(leave => { + let year = moment(leave.get_start_leave_day().date).format('YYYY'); + + if ( ! grouped_leaves[year]) { + grouped_leaves[ year ] = { + year : year, + leaves : [], + }; + } + + grouped_leaves[ year ].leaves.push(leave); + }); + + // Sort year groups + grouped_leaves = _ + .values( grouped_leaves ) + .sort((a,b) => a.year > b.year ? -1 : a.year < b.year ? 1 : 0); + + // Calculate total allowance deduction per group + grouped_leaves.forEach(group => { + group.total_deduction = _.reduce( + group.leaves.map(leave => leave.get_deducted_days_number()), + (memo, number) => memo + number, + 0 + ); + }); + + return Promise.resolve(grouped_leaves); +} + +/* + * Simple function that sorts array of Leave objects in default way. + * + * */ + +function promise_to_sort_leaves(leaves) { + return Promise.resolve( leaves.sort( + (a,b) => a.date_start > b.date_start + ? -1 : a.date_start < b.date_start + ? 1 : 0 + )); +} + +module.exports = function(){ + return { + promise_to_group_leaves : promise_to_group_leaves, + promise_to_sort_leaves : promise_to_sort_leaves, + }; +}; diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index 6ae432637..201d08916 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -8,10 +8,11 @@ 'use strict'; var - _ = require('underscore'), - Promise = require("bluebird"), - CalendarMonth = require('../../calendar_month'), - moment = require('moment'); + _ = require('underscore'), + Promise = require("bluebird"), + CalendarMonth = require('../../calendar_month'), + LeaveCollectionUtil = require('../../leave_collection')(), + moment = require('moment'); module.exports = function(sequelize){ @@ -245,7 +246,7 @@ module.exports = function(sequelize){ ); }) - .then(leaves => promise_to_sort_leaves(leaves)); + .then(leaves => LeaveCollectionUtil.promise_to_sort_leaves(leaves)); return promise_my_leaves; }; @@ -262,7 +263,7 @@ module.exports = function(sequelize){ sequelize.models.Leave.status_pended_revoke(), ], }) - .then(leaves => promise_to_sort_leaves(leaves)); + .then(leaves => LeaveCollectionUtil.promise_to_sort_leaves(leaves)); }; // Promises leaves ever booked for current user @@ -276,7 +277,7 @@ module.exports = function(sequelize){ sequelize.models.Leave.status_pended_revoke(), ], }) - .then(leaves => promise_to_sort_leaves(leaves)); + .then(leaves => LeaveCollectionUtil.promise_to_sort_leaves(leaves)); }; @@ -323,7 +324,7 @@ module.exports = function(sequelize){ { concurrency : 10 } ) .then( () => Promise.resolve(leaves) ) - .then(leaves => promise_to_sort_leaves(leaves)) + .then(leaves => LeaveCollectionUtil.promise_to_sort_leaves(leaves)) ); }; // END of promise_leaves_to_be_processed @@ -341,7 +342,7 @@ module.exports = function(sequelize){ concurrency : 10, }) .then(function(){ return Promise.resolve(leaves) }) - .then(leaves => promise_to_sort_leaves(leaves)); + .then(leaves => LeaveCollectionUtil.promise_to_sort_leaves(leaves)); }); }; @@ -686,15 +687,3 @@ module.exports = function(sequelize){ }; -/* - * Simple function that sorts array of Leave objects in default way. - * - * */ - -function promise_to_sort_leaves(leaves) { - return Promise.resolve( leaves.sort( - (a,b) => a.date_start > b.date_start - ? -1 : a.date_start < b.date_start - ? 1 : 0 - )); -} diff --git a/lib/route/requests.js b/lib/route/requests.js index d8ba0b77d..e7585671f 100644 --- a/lib/route/requests.js +++ b/lib/route/requests.js @@ -7,17 +7,20 @@ var express = require('express'), moment = require('moment'), validator = require('validator'), _ = require('underscore'), - EmailTransport = require('../email'); + LeaveCollectionUtil = require('../model/leave_collection')(), + EmailTransport = require('../email'); router.get('/', function(req, res){ Promise.join( - req.user.promise_my_active_leaves_ever(), + req.user + .promise_my_active_leaves_ever() + .then(leaves => LeaveCollectionUtil.promise_to_group_leaves(leaves)), req.user.promise_leaves_to_be_processed(), - function(my_leaves, to_be_approved_leaves){ + function(my_leaves_grouped, to_be_approved_leaves){ res.render('requests',{ - my_leaves : my_leaves, + my_leaves_grouped : my_leaves_grouped, to_be_approved_leaves : to_be_approved_leaves, }); } diff --git a/lib/route/users.js b/lib/route/users.js index cc4335ebf..04323677e 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -8,6 +8,7 @@ var express = require('express'), moment = require('moment'), _ = require('underscore'), uuid = require('node-uuid'), + LeaveCollectionUtil = require('../model/leave_collection')(), EmailTransport = require('../email'); // Make sure that current user is authorized to deal with settings @@ -199,15 +200,18 @@ router.get('/edit/:user_id/absences/', function(req, res){ .then(function(){ employee .promise_my_active_leaves_ever({}) - .then(function(leaves){ + .then(leaves => LeaveCollectionUtil.promise_to_group_leaves(leaves)) + .then(function(grouped_leaves){ + res.render('user_details', { employee : employee, - leaves : leaves, + grouped_leaves : grouped_leaves, show_absence_tab : true, leave_type_statistics : employee.get_leave_statistics_by_types(), - leave_statistics : leave_statistics, - employee_adjustment : employee_adjustment, + leave_statistics : leave_statistics, + employee_adjustment : employee_adjustment, }); + }); }); }) diff --git a/views/calendar.hbs b/views/calendar.hbs index 4de7f4cf9..d95936b94 100644 --- a/views/calendar.hbs +++ b/views/calendar.hbs @@ -140,9 +140,14 @@
- {{# unless show_full_year}} + +
+

All my absences in {{current_year}}

+
+ {{> user_requests leaves=current_user.my_leaves }} + {{/unless}} diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index ff63c9b99..89dc940f1 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -80,5 +80,6 @@
 
- {{> user_requests leaves=leaves no_header=1}} + {{> user_requests_grouped grouped_leaves=grouped_leaves logged_user=logged_user }} +
diff --git a/views/partials/user_requests.hbs b/views/partials/user_requests.hbs index 9479b338e..14736514c 100644 --- a/views/partials/user_requests.hbs +++ b/views/partials/user_requests.hbs @@ -1,10 +1,4 @@ -{{# unless no_header}} -
-

All absences

-
-{{/ unless}} -
{{# unless leaves}}
diff --git a/views/partials/user_requests_grouped.hbs b/views/partials/user_requests_grouped.hbs new file mode 100644 index 000000000..ba7f9c41b --- /dev/null +++ b/views/partials/user_requests_grouped.hbs @@ -0,0 +1,7 @@ + +{{# each grouped_leaves}} +

{{this.year}}

+ {{> user_requests leaves=this.leaves no_header=1 logged_user=../logged_user}} +

Days deducted from allowance: {{ this.total_deduction }}

+
 
+{{/ each}} diff --git a/views/requests.hbs b/views/requests.hbs index 4fa3d9979..31e8c6c80 100644 --- a/views/requests.hbs +++ b/views/requests.hbs @@ -60,6 +60,13 @@ {{/unless}}
-{{> user_requests leaves=my_leaves }} + +
 
+ +
+

All my absences

+
+ +{{> user_requests_grouped grouped_leaves=my_leaves_grouped logged_user=logged_user }} {{>footer}} From 58a3f03d940dec87f9ee76e80f5a899751b78cbb Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 22 Dec 2017 14:13:43 +0000 Subject: [PATCH 044/539] Add column to store carried over allowance --- lib/model/db/user_allowance_adjustment.js | 6 ++++ ...0171222-add-carry-over-allowance-column.js | 28 +++++++++++++++++++ views/partials/user_details/absences.hbs | 12 ++++++++ 3 files changed, 46 insertions(+) create mode 100644 migrations/20171222-add-carry-over-allowance-column.js diff --git a/lib/model/db/user_allowance_adjustment.js b/lib/model/db/user_allowance_adjustment.js index 8e58f3d82..4e35d890a 100644 --- a/lib/model/db/user_allowance_adjustment.js +++ b/lib/model/db/user_allowance_adjustment.js @@ -17,6 +17,12 @@ module.exports = function(sequelize, DataTypes){ defaultValue : 0, comment : 'Adjustment to allowance in current year', }, + carried_over_allowance : { + type : DataTypes.INTEGER, + allowNull : false, + defaultValue : 0, + comment : 'Additional allowance to use based on un-used holidays in previous year', + }, }, { underscored : true, freezeTableName : true, diff --git a/migrations/20171222-add-carry-over-allowance-column.js b/migrations/20171222-add-carry-over-allowance-column.js new file mode 100644 index 000000000..4c91e9a5e --- /dev/null +++ b/migrations/20171222-add-carry-over-allowance-column.js @@ -0,0 +1,28 @@ + +'use strict'; + +var models = require('../lib/model/db'); + +module.exports = { + up: function (queryInterface, Sequelize) { + + return queryInterface.describeTable('user_allowance_adjustment') + .then(function(attributes){ + + if (attributes.hasOwnProperty('carried_over_allowance')) { + return 1; + } + + return queryInterface.addColumn( + 'user_allowance_adjustment', + 'carried_over_allowance', + models.UserAllowanceAdjustment.attributes.carried_over_allowance + ); + }); + }, + + down: function (queryInterface, Sequelize) { + return queryInterface + .removeColumn('user_allowance_adjustment', 'carried_over_allowance'); + } +}; diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index 89dc940f1..fc94b4671 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -58,6 +58,18 @@
+
+ +
+ + working days +
+
+
Allowance adjustment based on unused holidays from previous year.
+
It is calculated at the beginning of current year.
+
+
+
From 5f1a76d8477eb4a6b21b7c3272fd3b6ea32166ef Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 22 Dec 2017 14:38:09 +0000 Subject: [PATCH 045/539] Take into consideration allowance carried over from previouse year --- lib/model/mixin/user/absence_aware.js | 42 +++++++++++++++++++----- lib/route/users.js | 10 ++++-- views/partials/user_details/absences.hbs | 2 +- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index 201d08916..7ba6b53ef 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -463,7 +463,8 @@ module.exports = function(sequelize){ let self = this, year = args.year, - adjustment = args.adjustment; + adjustment = args.adjustment, + carried_over_allowance = args.carried_over_allowance; // If optional paramater year was provided we need to calculate allowance // for that year, and if it is something other then current year, @@ -471,12 +472,14 @@ module.exports = function(sequelize){ // (ignoreing automatic allowance) if (year && year != moment().year()) { return self.department.allowance + + carried_over_allowance + adjustment; } // Get general allowance based on department return self.department.allowance + self.get_automatic_adjustment() + + carried_over_allowance + adjustment; }; @@ -498,18 +501,19 @@ module.exports = function(sequelize){ return Promise.resolve(); }) - // Fetch Adjustment for current year - .then(() => self.promise_adjustmet_for_year(year)) + // Fetch Adjustment and Carried over allowance for current year + .then(() => self.promise_adjustment_and_carry_over_for_year(year)) - .then(adjustment => Promise.resolve( + .then(adjustment_and_coa => Promise.resolve( self._calculate_total_number_of_days_n_allowance({ year : year, - adjustment : adjustment, + adjustment : adjustment_and_coa.adjustment, + carried_over_allowance : adjustment_and_coa.carried_over_allowance, }) )); }; - this.promise_adjustmet_for_year = function(year){ + this.promise_adjustment_and_carry_over_for_year = function(year){ let self = this; year = year || moment(); @@ -522,16 +526,36 @@ module.exports = function(sequelize){ .then(adjustment_records => { // By deafault there is not adjustments - let adjustment = 0; + let result = { + adjustment : 0, + carried_over_allowance : 0, + }; if (adjustment_records.length === 1) { - adjustment = adjustment_records[0].adjustment; + result.adjustment = adjustment_records[0].adjustment; + result.carried_over_allowance = adjustment_records[0].carried_over_allowance; } - return Promise.resolve(adjustment); + return Promise.resolve(result); }); }; + this.promise_adjustmet_for_year = function(year){ + let self = this; + + return self + .promise_adjustment_and_carry_over_for_year(year) + .then(combined_record => Promise.resolve(combined_record.adjustment)); + }; + + this.promise_carried_over_allowance_for_year = function(year){ + let self = this; + + return self + .promise_adjustment_and_carry_over_for_year(year) + .then(combined_record => Promise.resolve(combined_record.carried_over_allowance)); + }; + this.promise_to_update_adjustment = function(args) { let self = this, diff --git a/lib/route/users.js b/lib/route/users.js index 04323677e..ca351a5b5 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -167,9 +167,13 @@ router.get('/edit/:user_id/absences/', function(req, res){ employee .promise_adjustmet_for_year(moment().format('YYYY')), - (args, total_days_number, employee_adjustment) => { + employee + .promise_carried_over_allowance_for_year(moment().format('YYYY')), + + (args, total_days_number, employee_adjustment, carried_over_allowance) => { args.push(total_days_number); args.push(employee_adjustment); + args.push(carried_over_allowance); return Promise.resolve(args); }) ) @@ -178,7 +182,8 @@ router.get('/edit/:user_id/absences/', function(req, res){ remaining_allowance = args[0], employee = args[1], total_days_number = args[2], - employee_adjustment = args[3]; + employee_adjustment = args[3], + carried_over_allowance = args[4]; let leave_statistics = { total_for_current_year : total_days_number, @@ -210,6 +215,7 @@ router.get('/edit/:user_id/absences/', function(req, res){ leave_type_statistics : employee.get_leave_statistics_by_types(), leave_statistics : leave_statistics, employee_adjustment : employee_adjustment, + carried_over_allowance: carried_over_allowance, }); }); diff --git a/views/partials/user_details/absences.hbs b/views/partials/user_details/absences.hbs index fc94b4671..4cb21b565 100644 --- a/views/partials/user_details/absences.hbs +++ b/views/partials/user_details/absences.hbs @@ -61,7 +61,7 @@
- + working days
From 2ee957d018b0bdae3b19cf432c9e8476a94dcfbc Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 22 Dec 2017 15:42:48 +0000 Subject: [PATCH 046/539] Leaves list is less cluttering --- views/partials/user_requests.hbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/partials/user_requests.hbs b/views/partials/user_requests.hbs index 14736514c..1b094046a 100644 --- a/views/partials/user_requests.hbs +++ b/views/partials/user_requests.hbs @@ -12,9 +12,9 @@ + - @@ -25,9 +25,9 @@ {{# each leaves }} + - {{# each users_and_leaves}} - + diff --git a/views/requests.hbs b/views/requests.hbs index 31e8c6c80..560a253cc 100644 --- a/views/requests.hbs +++ b/views/requests.hbs @@ -35,7 +35,7 @@ - + From 0355ee7ac25364dde02670aa8f0ced7f208544b3 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 28 Dec 2017 19:40:15 +0000 Subject: [PATCH 058/539] Make Calenda and team view pages be TZ aware --- lib/model/calendar_month.js | 8 +++++++- lib/model/db/department.js | 1 + lib/model/mixin/user/absence_aware.js | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/model/calendar_month.js b/lib/model/calendar_month.js index d944356c1..775ff3f87 100644 --- a/lib/model/calendar_month.js +++ b/lib/model/calendar_month.js @@ -10,6 +10,12 @@ function CalendarMonth(day, args){ this._leaves = {}; this._bank_holidays = {}; + this.today = moment.utc(); + + if (args && args.today) { + this.today = args.today; + } + if (args){ this._schedule = args.schedule; } @@ -146,7 +152,7 @@ CalendarMonth.prototype.is_weekend = function(day){ CalendarMonth.prototype.is_current_day = function(day){ return this.get_base_date().date(day).format( this.default_date_format() ) === - moment.utc().format( this.default_date_format() ); + this.today.format( this.default_date_format() ); }; CalendarMonth.prototype.as_for_template = function(){ diff --git a/lib/model/db/department.js b/lib/model/db/department.js index 27d592c0d..632c7c87e 100644 --- a/lib/model/db/department.js +++ b/lib/model/db/department.js @@ -159,6 +159,7 @@ module.exports = function(sequelize, DataTypes) { : [], leave_days : user_data.leave_days, schedule : user_data.schedule, + today : company.get_today(), }); user_data.days = calendar_month.as_for_team_view(); diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index 2433cb415..7ca88909d 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -115,6 +115,7 @@ module.exports = function(sequelize){ : [], leave_days : leave_days, schedule : schedule, + today : company.get_today(), } ); }) From a54f811c8d3e897cf50004a7551c9bc2cb65a80c Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 28 Dec 2017 21:29:18 +0000 Subject: [PATCH 059/539] Ensure app uses TZ aware now getter Although cases when it is YEAR that is considered are ignored. --- lib/model/calendar_month.js | 3 ++- lib/model/db/department.js | 19 ++++++++++++++---- lib/model/db/user.js | 3 +-- lib/model/mixin/user/absence_aware.js | 28 +++++++++++++++------------ lib/model/team_view.js | 2 +- lib/route/calendar.js | 4 ++-- lib/route/feed.js | 2 +- lib/route/users.js | 2 +- t/unit/calendar.js | 25 ++++++++++++------------ 9 files changed, 52 insertions(+), 36 deletions(-) diff --git a/lib/model/calendar_month.js b/lib/model/calendar_month.js index 775ff3f87..2ed9a8850 100644 --- a/lib/model/calendar_month.js +++ b/lib/model/calendar_month.js @@ -10,10 +10,11 @@ function CalendarMonth(day, args){ this._leaves = {}; this._bank_holidays = {}; - this.today = moment.utc(); if (args && args.today) { this.today = args.today; + } else { + throw new Error('CalendarMonth requires today - moment object that represents today'); } if (args){ diff --git a/lib/model/db/department.js b/lib/model/db/department.js index 632c7c87e..99e531802 100644 --- a/lib/model/db/department.js +++ b/lib/model/db/department.js @@ -99,12 +99,23 @@ module.exports = function(sequelize, DataTypes) { var self = this, model = sequelize.models, - // NOTE VPP: following date is going to be amended by user's time zone - base_date = args.base_date || moment.utc(); + base_date = args.base_date; - var promise_users_and_leaves = Promise.try(function(){ - return self.promise_active_users(); + + var promise_users_and_leaves = Promise + + // First of all ensure that "base_date" is defined + .try(function(){ + if ( base_date ) { + return Promise.resolve(base_date); + } + + return self.getCompany() + .then(company => Promise.resolve( base_date = company.get_today() ) ) }) + + // Get users + .then( () => self.promise_active_users() ) .then(function(users){ return Promise.all( diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 5cc936dd4..8b0d5da5c 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -282,8 +282,7 @@ function get_instance_methods(sequelize) { }, reload_with_leave_details : function(args){ - var year = args.year || moment.utc(), - self = this; + var self = this; return Promise.join( self.promise_my_active_leaves(args), diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index 7ca88909d..a7f9a0eef 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -17,8 +17,10 @@ var module.exports = function(sequelize){ this._get_calendar_months_to_show = function(args){ - var year = args.year, - show_full_year = args.show_full_year; + var + self = this, + year = args.year, + show_full_year = args.show_full_year; if (show_full_year) { return _.map([1,2,3,4,5,6,7,8,9,10,11,12], function(i){ @@ -27,18 +29,19 @@ module.exports = function(sequelize){ } return _.map([0,1,2,3], function(delta){ - return moment.utc().add(delta, 'months').startOf('month'); + return self.company.get_today().add(delta, 'months').startOf('month'); }) }; this.promise_calendar = function(args) { - var year = args.year || moment.utc(), - show_full_year = args.show_full_year || false, - model = sequelize.models, - this_user = this, - // Find or if we need to show multi year calendar - is_multi_year = moment.utc().month() > 8; + var + this_user = this, + year = args.year || this_user.company.get_today(), + show_full_year = args.show_full_year || false, + model = sequelize.models, + // Find out if we need to show multi year calendar + is_multi_year = this_user.company.get_today().month() > 8; var months_to_show = this_user._get_calendar_months_to_show({ year : year.clone(), @@ -182,13 +185,14 @@ module.exports = function(sequelize){ var self = this, where_clause = {}, + // Time zone does not really matter here, although there could be issues + // around New Year (but we can tolerate that) year = args.year || moment.utc(); if (args && args.filter_status) { where_clause = { status : args.filter_status }; } - if (args && ! args.ignore_year) { where_clause['$or'] = { date_start : { @@ -439,7 +443,7 @@ module.exports = function(sequelize){ this.get_automatic_adjustment = function(args) { - var now = (args && args.now) ? moment.utc(args.now) : moment.utc(); + var now = (args && args.now) ? moment.utc(args.now) : this.company.get_today(); if ( now.year() !== moment.utc(this.start_date).year() @@ -613,7 +617,7 @@ module.exports = function(sequelize){ this.promise_my_leaves_for_calendar = function(args){ - var year = args.year || moment.utc(); + var year = args.year || this.company.get_today(); return this.getMy_leaves({ where : { diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 25eee0be3..90d11d6aa 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -10,8 +10,8 @@ const function TeamView(args) { var me = this; - this.base_date = args.base_date || moment.utc(); this.user = args.user; + this.base_date = args.base_date || this.user.company.get_today(); } TeamView.prototype.promise_team_view_details = function(args){ diff --git a/lib/route/calendar.js b/lib/route/calendar.js index c0fe7eee2..0efd4c71b 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -98,7 +98,7 @@ router.get('/', function(req, res) { var current_year = validator.isNumeric(req.param('year')) ? moment.utc(req.param('year'), 'YYYY') - : moment.utc(); + : req.user.company.get_today(); var show_full_year = validator.toBoolean(req.param('show_full_year')); @@ -145,7 +145,7 @@ router.get('/teamview/', function(req, res){ var base_date = validator.isDate(req.param('date')) ? moment.utc(req.param('date')) - : moment.utc(); + : req.user.company.get_today(); var team_view = new TeamView({ base_date : base_date, diff --git a/lib/route/feed.js b/lib/route/feed.js index 03f4522be..22b71ed7d 100644 --- a/lib/route/feed.js +++ b/lib/route/feed.js @@ -44,7 +44,7 @@ router.get('/:token/ical.ics', function(req, res){ if (feed.is_calendar()){ return user.promise_calendar({ - year : moment.utc(), + year : user.calendar.company.get_today(), show_full_year : true, }) .then(function(calendar){ diff --git a/lib/route/users.js b/lib/route/users.js index 1dece7612..e028323ac 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -291,7 +291,7 @@ var ensure_user_was_not_useed_elsewhere_while_being_inactive = function(args){ // new "end_date" is provided // new "end_date" is in future new_user_attributes.end_date && - moment.utc( new_user_attributes.end_date ).startOf('day').toDate() >= moment.utc().startOf('day').toDate() + moment.utc( new_user_attributes.end_date ).startOf('day').toDate() >= req.user.company.get_today().startOf('day').toDate() ) ) ) { diff --git a/t/unit/calendar.js b/t/unit/calendar.js index 7a4f46c69..f53ea8b3b 100644 --- a/t/unit/calendar.js +++ b/t/unit/calendar.js @@ -4,6 +4,7 @@ var expect = require('chai').expect, _ = require('underscore'), model = require('../../lib/model/db'), + moment = require('moment'), schedule= model.Schedule.build({ company_id : 1 }), CalendarMonth = require('../../lib/model/calendar_month'); @@ -11,7 +12,7 @@ var expect = require('chai').expect, describe('Check calendar month object', function(){ it('Normalize provided date to be at the begining of the month',function(){ - var january = new CalendarMonth('2015-01-10', {schedule : schedule}); + var january = new CalendarMonth('2015-01-10', {schedule : schedule, today : moment.utc()}); expect( january.get_base_date().date() @@ -19,31 +20,31 @@ describe('Check calendar month object', function(){ }); it('Knows on which week day month starts', function(){ - var january = new CalendarMonth('2015-01-21', {schedule : schedule}); + var january = new CalendarMonth('2015-01-21', {schedule : schedule, today : moment.utc()}); expect( january.week_day() ).to.be.equal(4); - var feb = new CalendarMonth('2015-02-21', {schedule : schedule}); + var feb = new CalendarMonth('2015-02-21', {schedule : schedule, today : moment.utc()}); expect( feb.week_day() ).to.be.equal(7); }); it('Knows how many blanks to put before first day of the month', function(){ - var january = new CalendarMonth('2015-01-11', {schedule : schedule}); + var january = new CalendarMonth('2015-01-11', {schedule : schedule, today : moment.utc()}); expect( january.how_many_blanks_at_the_start() ).to.be.equal(3); - var feb = new CalendarMonth('2015-02-11', {schedule : schedule}); + var feb = new CalendarMonth('2015-02-11', {schedule : schedule, today : moment.utc()}); expect( feb.how_many_blanks_at_the_start() ).to.be.equal(6); }); it('Knows how many blanks to put after the last day of the month', function(){ - var january = new CalendarMonth('2015-01-11', {schedule : schedule}); + var january = new CalendarMonth('2015-01-11', {schedule : schedule, today : moment.utc()}); expect( january.how_many_blanks_at_the_end() ).to.be.equal(1); - var feb = new CalendarMonth('2015-02-11', {schedule : schedule}); + var feb = new CalendarMonth('2015-02-11', {schedule : schedule, today : moment.utc()}); expect( feb.how_many_blanks_at_the_end() ).to.be.equal(1); }); it('Knows whether day is weekend', function(){ - var feb = new CalendarMonth('2015-02-12', {schedule : schedule}); + var feb = new CalendarMonth('2015-02-12', {schedule : schedule, today : moment.utc()}); expect(feb.is_weekend(12)).not.to.be.ok; expect(feb.is_weekend(21)).to.be.ok; expect(feb.is_weekend(22)).to.be.ok; @@ -51,7 +52,7 @@ describe('Check calendar month object', function(){ }); it('Knows how to generate data structure for template', function(){ - var january = new CalendarMonth('2015-01-11', { schedule : schedule }), + var january = new CalendarMonth('2015-01-11', { schedule : schedule, today : moment.utc() }), object_to_test = january.as_for_template(); delete object_to_test['moment']; object_to_test.weeks.forEach(function(week){ @@ -62,7 +63,7 @@ describe('Check calendar month object', function(){ ); - var apr = new CalendarMonth('2015-04-11', { schedule : schedule }); + var apr = new CalendarMonth('2015-04-11', { schedule : schedule, today : moment.utc() }); object_to_test = apr.as_for_template(); delete object_to_test['moment']; object_to_test.weeks.forEach(function(week){ @@ -77,7 +78,7 @@ describe('Check calendar month object', function(){ it('Sanity checks pass', function(){ - var apr = new CalendarMonth('2015-04-01', { schedule : schedule }); + var apr = new CalendarMonth('2015-04-01', { schedule : schedule, today : moment.utc() }); expect(apr).to.be.a('object'); @@ -85,7 +86,7 @@ describe('Check calendar month object', function(){ }); it('It knows whether day is bank holiday', function(){ - var mar = new CalendarMonth('2015-03-19', { bank_holidays : ['2015-03-08'], schedule : schedule }); + var mar = new CalendarMonth('2015-03-19', { bank_holidays : ['2015-03-08'], schedule : schedule, today : moment.utc() }); expect(mar.is_bank_holiday(8)).to.be.ok; expect(mar.is_bank_holiday(10)).not.to.be.ok; From fe087ac053cab7ed8edd04f5e8a567f40845369f Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 2 Jan 2018 14:46:05 +0000 Subject: [PATCH 060/539] Sketch scenario for test to be --- t/integration/timezone.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 t/integration/timezone.js diff --git a/t/integration/timezone.js b/t/integration/timezone.js new file mode 100644 index 000000000..67a5496e7 --- /dev/null +++ b/t/integration/timezone.js @@ -0,0 +1,21 @@ + +'use strict'; + +/* + * Basic scenario for checking time zones: + * + * * Create a copany + * * Update Time zone to be somethng in Australia + * * Get the date from Book leave modal and put it into today_aus + * * Get the current date from Calendar page and ensure it is the same as today_aus + * * Get the current date from Team view page and ensure it is the same as today_aus + * * Book a leave and ensure its "created at" value on My requests page is today_aus + * * Reject newly added leave + * * Update Time zone to be USA/Alaska + * * Get the date from Book leave modal and put it into today_usa + * * Ensure that today_usa is one day behind the today_aus + * * Get the current date from Calendar page and ensure it is the same as today_usa + * * Get the current date from Team view page and ensure it is the same as today_usa + * * Book a leave and ensure its "created at" value on My requests page is today_usa + * + * */ From 46e623aa705d61b4a67e7546cbb569731e0e9b2b Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 3 Jan 2018 20:29:07 +0000 Subject: [PATCH 061/539] Add test for time zones --- t/integration/leave_request/cancel_basic.js | 2 +- t/integration/timezone.js | 254 ++++++++++++++++++++ views/requests.hbs | 4 +- 3 files changed, 257 insertions(+), 3 deletions(-) diff --git a/t/integration/leave_request/cancel_basic.js b/t/integration/leave_request/cancel_basic.js index 4ed3ec259..7f605f084 100644 --- a/t/integration/leave_request/cancel_basic.js +++ b/t/integration/leave_request/cancel_basic.js @@ -117,7 +117,7 @@ describe('Leave request cancelation', function(){ .then(function(){ done() }); }); - it("Submit new leave requesti from user B for one weekday", function(done){ + it("Submit new leave request from user B for one weekday", function(done){ submit_form_func({ driver : driver, form_params : [{ diff --git a/t/integration/timezone.js b/t/integration/timezone.js index 67a5496e7..06ba71f97 100644 --- a/t/integration/timezone.js +++ b/t/integration/timezone.js @@ -1,6 +1,20 @@ 'use strict'; +const + test = require('selenium-webdriver/testing'), + register_new_user_func = require('../lib/register_new_user'), + open_page_func = require('../lib/open_page'), + submit_form_func = require('../lib/submit_form'), + check_elements_func = require('../lib/check_elements'), + By = require('selenium-webdriver').By, + config = require('../lib/config'), + application_host = config.get_application_host(), + expect = require('chai').expect, + Bluebird = require('bluebird'), + moment = require('moment'), + company_edit_form_id ='#company_edit_form'; + /* * Basic scenario for checking time zones: * @@ -19,3 +33,243 @@ * * Book a leave and ensure its "created at" value on My requests page is today_usa * * */ + +describe('Check departments list page', function(){ + let + driver, + user_email, + today_usa, + today_aus; + + this.timeout( config.get_execution_timeout() ); + + it("Create a company", function(done){ + register_new_user_func({ + application_host : application_host, + }) + .then(function(data){ + driver = data.driver; + user_email = data.email; + done(); + }); + }); + + it("Open page for editing company details", function(done){ + open_page_func({ + url : application_host + 'settings/general/', + driver : driver, + }) + .then(() => done() ); + }); + + it("Update Time zone to be somethng in Australia", function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : company_edit_form_id+' select[name="timezone"]', + option_selector : 'option[value="Australia/Sydney"]', + value : 'Australia/Sydney', + }], + submit_button_selector : company_edit_form_id+' button[type="submit"]', + message : /successfully/i, + should_be_successful : true, + }) + .then(function(){ done() }); + }); + + it("Get the date from Book leave modal and put it into today_aus variable", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then(el => el.click()) + // This is very important line when working with Bootstrap modals! + .then(() => driver.sleep(1000)) + .then(() => driver.findElement(By.css( 'input.book-leave-from-input' ))) + .then(el => el.getAttribute('value')) + .then(today => { + today_aus = today; + done(); + }); + }); + + it("Get the current date from Calendar page and ensure it is the same as today_aus", function(done){ + open_page_func({ + url : application_host + 'calendar/', + driver : driver, + }) + .then(() => driver.findElement(By.css( + 'table.month_'+moment(today_aus).format('MMMM') + + ' td.half_1st.day_'+moment(today_aus).format('D')+'.current_day_cell' + ))) + .then(el => { + expect(el, 'Ensure that current date is marked correctly').to.exist; + done(); + }); + }); + + it("Get the current date from Team view page and ensure it is the same as today_aus", function(done){ + open_page_func({ + url : application_host + 'calendar/teamview/', + driver : driver, + }) + .then(() => driver.findElement(By.css( + 'table.calendar_month td.half_1st.day_'+moment(today_aus).format('D')+'.current_day_cell' + ))) + .then(el => { + expect(el, 'Ensure that current date is marked correctly').to.exist; + + return driver.findElement(By.css('div.calendar-section-caption')) + }) + .then(el => el.getText()) + .then(month_caption => { + expect(month_caption, 'Ensure month is correct').to.be.eql(moment(today_aus).format('MMMM, YYYY')); + done(); + }) + }); + + it("Open Book leave popup window", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then( el => el.click() ) + // This is very important line when working with Bootstrap modals! + .then(el => driver.sleep(1000)) + .then(() => done()); + }); + + it("Submit new leave request", function(done){ + submit_form_func({ + driver : driver, + form_params : [], + message : /New leave request was added/, + }) + .then(() => done()); + }); + + it('Ensure its "created at" value on My requests page is today_aus', function( done ){ + open_page_func({ + url : application_host + 'requests/', + driver : driver, + }) + .then(() => driver.findElement(By.css('tr[vpp="pending_for__'+user_email+'"] td.date_of_request'))) + .then(el => el.getText()) + .then(text => { + expect(text).to.be.eql(moment(today_aus).format('YYYY-MM-DD')); + done(); + }); + }); + + it('Reject newly added leave', function(done){ + driver + .findElement(By.css('tr[vpp="pending_for__'+user_email+'"] input[value="Reject"]')) + .then(el => el.click()) + .then(() => done()); + }); + + it("Open page for editing company details", function(done){ + open_page_func({ + url : application_host + 'settings/general/', + driver : driver, + }) + .then(() => done() ); + }); + + it("Update Time zone to be USA/Alaska", function(done){ + submit_form_func({ + driver : driver, + form_params : [{ + selector : company_edit_form_id+' select[name="timezone"]', + option_selector : 'option[value="US/Alaska"]', + value : 'US/Alaska', + }], + submit_button_selector : company_edit_form_id+' button[type="submit"]', + message : /successfully/i, + should_be_successful : true, + }) + .then(function(){ done() }); + }); + + it("Get the date from Book leave modal and put it into today_usa", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then(el => el.click()) + // This is very important line when working with Bootstrap modals! + .then(() => driver.sleep(1000)) + .then(() => driver.findElement(By.css( 'input.book-leave-from-input' ))) + .then(el => el.getAttribute('value')) + .then(today => { + today_usa = today; + done(); + }); + }); + + it("Ensure that today_usa is one day behind the today_aus", function(done){ + expect(moment(today_usa).format('YYYY-MM-DD')).to.be.not.eql(moment(today_aus).format('YYYY-MM-DD')); + done(); + }); + + it("Get the current date from Calendar page and ensure it is the same as today_usa", function(done){ + open_page_func({ + url : application_host + 'calendar/', + driver : driver, + }) + .then(() => driver.findElement(By.css( + 'table.month_'+moment(today_usa).format('MMMM') + + ' td.half_1st.day_'+moment(today_usa).format('D')+'.current_day_cell' + ))) + .then(el => { + expect(el, 'Ensure that current date is marked correctly').to.exist; + done(); + }); + }); + + it("Get the current date from Team view page and ensure it is the same as today_usa", function(done){ + open_page_func({ + url : application_host + 'calendar/teamview/', + driver : driver, + }) + .then(() => driver.findElement(By.css( + 'table.calendar_month td.half_1st.day_'+moment(today_usa).format('D')+'.current_day_cell' + ))) + .then(el => { + expect(el, 'Ensure that current date is marked correctly').to.exist; + + return driver.findElement(By.css('div.calendar-section-caption')) + }) + .then(el => el.getText()) + .then(month_caption => { + expect(month_caption, 'Ensure month is correct').to.be.eql(moment(today_usa).format('MMMM, YYYY')); + done(); + }) + }); + + it("Open Book leave popup window", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then( el => el.click() ) + // This is very important line when working with Bootstrap modals! + .then(el => driver.sleep(1000)) + .then(() => done()); + }); + + it("Submit new leave request", function(done){ + submit_form_func({ + driver : driver, + form_params : [], + message : /New leave request was added/, + }) + .then(() => done()); + }); + + it('Ensure its "created at" value on My requests page is today_usa', function( done ){ + open_page_func({ + url : application_host + 'requests/', + driver : driver, + }) + .then(() => driver.findElement(By.css('tr[vpp="pending_for__'+user_email+'"] td.date_of_request'))) + .then(el => el.getText()) + .then(text => { + expect(text).to.be.eql(moment(today_usa).format('YYYY-MM-DD')); + done(); + }); + }); + + after(function(done){ + driver.quit().then(() => done()); + }); + +}); diff --git a/views/requests.hbs b/views/requests.hbs index 560a253cc..793f4803b 100644 --- a/views/requests.hbs +++ b/views/requests.hbs @@ -35,8 +35,8 @@ - - + + - {{# each users_info}} - {{# with this.user_row}} - - - - - - + + + + + + - {{/with}} {{/each}}
Dates (from to) Type DeductedDates Approved by Status
{{#with this.get_start_leave_day}}{{as_date this.date}}{{/with}} {{#with this.get_end_leave_day}}{{as_date this.date}}{{/with}} {{ this.leave_type.name }}{{#if this.is_pended_revoke_leave}}
(pended revoke){{/if}}
{{ this.get_deducted_days_number }}From {{#with this.get_start_leave_day}}{{as_date this.date}}{{/with}} to {{#with this.get_end_leave_day}}{{as_date this.date}}{{/with}} {{#with this.approver}}{{this.full_name}}{{/with}} {{# if this.is_approved_leave }} From d069d75712e41d7cf59c9f389a551aa0aa149671 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 22 Dec 2017 16:06:20 +0000 Subject: [PATCH 047/539] Leave mark for future improvements --- lib/model/mixin/user/company_aware.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/model/mixin/user/company_aware.js b/lib/model/mixin/user/company_aware.js index 771e55147..17d4ef0e0 100644 --- a/lib/model/mixin/user/company_aware.js +++ b/lib/model/mixin/user/company_aware.js @@ -21,6 +21,10 @@ module.exports = function(sequelize){ * for user determined by user_id. * Returns promise that is resolved with company object as parameter */ + // + // TODO: Query below needs to be revisited as it is slow for users + // many leaves + // this.get_company_for_user_details = function(args){ var user_id = args.user_id, year = args.year || moment(), From dd1bd407fbd917732ef66ef57d6413cd6d169e36 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 22 Dec 2017 18:52:22 +0000 Subject: [PATCH 048/539] Add script that carries over remaning allowance into next year --- ...late_carry_over_allowance_for_all_users.js | 41 +++++++++++++++++++ lib/model/mixin/user/absence_aware.js | 27 ++++++++++++ package.json | 1 + 3 files changed, 69 insertions(+) create mode 100644 bin/calculate_carry_over_allowance_for_all_users.js diff --git a/bin/calculate_carry_over_allowance_for_all_users.js b/bin/calculate_carry_over_allowance_for_all_users.js new file mode 100644 index 000000000..df81ea911 --- /dev/null +++ b/bin/calculate_carry_over_allowance_for_all_users.js @@ -0,0 +1,41 @@ + +'use strict'; + +const + Promise = require('bluebird'), + models = require('../lib/model/db'); + +const + YEAR_FROM = '2017', + YEAR_TO = '2018'; + +/* + * 1. Get all users + * + * 2. Iterate through users and: + * + * 3. Calculate remaining days for current year + * + * 4. Put value from step 3 into user_allowance_adjustment.carried_over_allowance + * of next year + * + * */ + +models.User + .findAll() + .then(users => Promise.map( + users, + user => { + return user + .reload_with_leave_details({YEAR_FROM}) + .then( user => user.promise_number_of_days_available_in_allowance(YEAR_FROM) ) + .then(remainer => { + return user.promise_to_update_carried_over_allowance({ + year : YEAR_TO, + carried_over_allowance : remainer, + }); + }) + .then(() => Promise.resolve(console.log('Done with user ' + user.id))); + }, + {concurrency : 1} + )); diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index 7ba6b53ef..d4f10ba47 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -583,6 +583,33 @@ module.exports = function(sequelize){ }); }; + this.promise_to_update_carried_over_allowance = function(args) { + let + self = this, + year = args.year || moment().format('YYYY'), + carried_over_allowance = args.carried_over_allowance; + + // Update related allowance adjustement record + return sequelize.models.UserAllowanceAdjustment + .findOrCreate({ + where : { + user_id : self.id, + year : year, + }, + defaults : { carried_over_allowance : carried_over_allowance }, + }) + .spread((record, created) => { + + if ( created ) { + return Promise.resolve(); + } + + record.set('carried_over_allowance', carried_over_allowance); + + return record.save(); + }); + }; + this.promise_my_leaves_for_calendar = function(args){ var year = args.year || moment(); diff --git a/package.json b/package.json index 03585aeb9..d9ca48a01 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "test": "node node_modules/mocha/bin/mocha --recursive t/*", "start": "node bin/wwww", "db-update": "node node_modules/.bin/sequelize db:migrate --config=config/db.json --models-path=lib/model/db/", + "carry-over-allowance" : "node bin/calculate_carry_over_allowance_for_all_users.js", "compile-sass": "node node_modules/node-sass/bin/node-sass scss/main.scss public/css/style.css" } } From c5ef8319e40c3e2d65b17adbfa4bd0e19e992aa2 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 22 Dec 2017 19:45:28 +0000 Subject: [PATCH 049/539] Fix issue with UI on Team view when department has long name --- public/css/style.css | 2 +- scss/main.scss | 2 +- t/integration/team_view/basic_wall_chart.js | 2 +- views/partials/user_requests_grouped.hbs | 2 +- views/team_view.hbs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index bcbc8ea2c..d666ed174 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -113,7 +113,7 @@ td.leave_cell_pended { padding-top: 5px; padding-bottom: 5px; } -.team-view-users .user-name-cell { +.team-view-users .left-column-cell { text-overflow: ellipsis; /* Cannot avoid specifying width with pixels... */ width: 165px; diff --git a/scss/main.scss b/scss/main.scss index 7c3541954..917286924 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -141,7 +141,7 @@ td.leave_cell_pended { padding-bottom : 5px; } -.team-view-users .user-name-cell { +.team-view-users .left-column-cell { text-overflow: ellipsis; /* Cannot avoid specifying width with pixels... */ width: 165px; diff --git a/t/integration/team_view/basic_wall_chart.js b/t/integration/team_view/basic_wall_chart.js index 4363885d4..d5da102d9 100644 --- a/t/integration/team_view/basic_wall_chart.js +++ b/t/integration/team_view/basic_wall_chart.js @@ -54,7 +54,7 @@ function check_teamview(data, emails){ }) .then(function(data){ var promise_to_check = data.driver - .findElements(By.css( 'tr.teamview-user-list-row > td.user-name-cell' )) + .findElements(By.css( 'tr.teamview-user-list-row > td.left-column-cell' )) // Make sure that number of users is as expected .then(function(elements){ diff --git a/views/partials/user_requests_grouped.hbs b/views/partials/user_requests_grouped.hbs index ba7f9c41b..ad1795a1e 100644 --- a/views/partials/user_requests_grouped.hbs +++ b/views/partials/user_requests_grouped.hbs @@ -2,6 +2,6 @@ {{# each grouped_leaves}}

{{this.year}}

{{> user_requests leaves=this.leaves no_header=1 logged_user=../logged_user}} -

Days deducted from allowance: {{ this.total_deduction }}

+

Days would be deducted from allowance: {{ this.total_deduction }}

 
{{/ each}} diff --git a/views/team_view.hbs b/views/team_view.hbs index 315ce73d1..272a064a0 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -31,7 +31,7 @@
{{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}}{{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}} Date: Fri, 22 Dec 2017 21:06:43 +0000 Subject: [PATCH 050/539] Clean up pagination widget Make it simple --- views/partials/pager.hbs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/views/partials/pager.hbs b/views/partials/pager.hbs index f2561e547..e4f4362e5 100644 --- a/views/partials/pager.hbs +++ b/views/partials/pager.hbs @@ -20,24 +20,28 @@
  • {{#if pager.page_prev}} - « + « Prev {{else}} - « + « Prev {{/if}}
  • {{# each pager.pages }} -
  • - {{ this }} -
  • + + {{# if_equal this ../pager.page }} +
  • + {{ this }} of {{ ../pager.page_qnty }} +
  • + {{/ if_equal}} + {{/ each}} {{#if pager.page_next}} - » + Next » {{else}} - » + Next » {{/if}} From c7df06c825716d7f459ab3e1a856ce3871625460 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 2 Jan 2018 16:39:14 +0000 Subject: [PATCH 051/539] Adopt tests for new year Also add new filed for setting company into read only mode --- lib/model/db/company.js | 21 ++++++++ lib/route/calendar.js | 54 +++++++++++-------- ...maining_used_columns_match_user_details.js | 4 +- t/integration/leave_request/cancel_basic.js | 2 +- .../leave_request/leave_request_revoke.js | 8 +-- .../leave_request_revoke_by_admin.js | 8 +-- t/integration/leave_type_limit_next_year.js | 4 +- t/unit/model/db/user.js | 12 ++--- 8 files changed, 71 insertions(+), 42 deletions(-) diff --git a/lib/model/db/company.js b/lib/model/db/company.js index 209ca54c5..2d17e6cf2 100644 --- a/lib/model/db/company.js +++ b/lib/model/db/company.js @@ -66,6 +66,12 @@ module.exports = function(sequelize, DataTypes) { defaultValue : null, comment : 'Message shown to all users that belong to current company', }, + mode : { + type : DataTypes.INTEGER, + allowNull : false, + defaultValue : 1, + comment : "Indicate which mode the company account is in.", + }, }, { indexes : [ @@ -148,6 +154,14 @@ module.exports = function(sequelize, DataTypes) { // }); }, + // Return code for "read-only holidays" mode of company account. + // That means company only shows holidays/timeoff for emplyes and + // does not allow to create new ones. + // + get_mode_readonly_holidays : function(){ + return 2; + }, + // Create new company based on default values create_default_company : function(args){ var country_code = args.country_code || 'UK'; @@ -396,6 +410,13 @@ module.exports = function(sequelize, DataTypes) { }); }, + // Return TRUE if company has restrictio on ly to show hollidays for its + // employees and prevent them from adding new ones + // + is_mode_readonly_holidays : function(){ + return this.mode === Company.get_mode_readonly_holidays(); + }, + } }); diff --git a/lib/route/calendar.js b/lib/route/calendar.js index cb077901b..b8d48bc58 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -16,30 +16,38 @@ router.post('/bookleave/', function(req, res){ var model = req.app.get('db_model'); Promise.join ( - req.user.promise_users_I_can_manage(), - req.user.get_company_with_all_leave_types(), - Promise.try( function(){return get_and_validate_leave_params({req : req})}), - function(users, company, valide_attributes){ - // Make sure that indexes submitted map to existing objects - var employee = users[valide_attributes.user] || req.user, - leave_type = company.leave_types[valide_attributes.leave_type]; - - if (!employee) { - req.session.flash_error('Incorrect employee'); - throw new Error( 'Got validation errors' ); - } - - if (!leave_type) { - req.session.flash_error('Incorrect leave type'); - throw new Error( 'Got validation errors' ); - } - - return model.Leave.create_new_leave({ - for_employee : employee, - of_type : leave_type, - with_parameters : valide_attributes, - }); + req.user.promise_users_I_can_manage(), + req.user.get_company_with_all_leave_types(), + Promise.try( function(){return get_and_validate_leave_params({req : req})}), + function(users, company, valide_attributes){ + // Make sure that indexes submitted map to existing objects + var employee = users[valide_attributes.user] || req.user, + leave_type = company.leave_types[valide_attributes.leave_type]; + + if (!employee) { + req.session.flash_error('Incorrect employee'); + throw new Error( 'Got validation errors' ); } + + if (!leave_type) { + req.session.flash_error('Incorrect leave type'); + throw new Error( 'Got validation errors' ); + } + + if (company.is_mode_readonly_holidays() ){ + req.session.flash_error( + "Company account is locked and new Timeoff " + + "requests could not be added. Please contact administration." + ); + throw new Error('Company is in "Read-only holidays" mode'); + } + + return model.Leave.create_new_leave({ + for_employee : employee, + of_type : leave_type, + with_parameters : valide_attributes, + }); + } ) .then(function(leave){ diff --git a/t/integration/employees_page/remaining_used_columns_match_user_details.js b/t/integration/employees_page/remaining_used_columns_match_user_details.js index 8891c558e..313805179 100644 --- a/t/integration/employees_page/remaining_used_columns_match_user_details.js +++ b/t/integration/employees_page/remaining_used_columns_match_user_details.js @@ -107,10 +107,10 @@ describe('Leave request cancelation', function(){ driver : driver, form_params : [{ selector : 'input#from', - value : '2017-05-01', + value : '2018-05-01', },{ selector : 'input#to', - value : '2017-05-07', + value : '2018-05-07', }], message : /New leave request was added/, }) diff --git a/t/integration/leave_request/cancel_basic.js b/t/integration/leave_request/cancel_basic.js index 629c410ae..4ed3ec259 100644 --- a/t/integration/leave_request/cancel_basic.js +++ b/t/integration/leave_request/cancel_basic.js @@ -17,7 +17,7 @@ var test = require('selenium-webdriver/testing'), submit_form_func = require('../../lib/submit_form'), user_info_func = require('../../lib/user_info'), application_host = config.get_application_host(), - some_weekday_date = '2017-06-01'; + some_weekday_date = '2018-01-03'; /* * Scenario: diff --git a/t/integration/leave_request/leave_request_revoke.js b/t/integration/leave_request/leave_request_revoke.js index 710286ae5..6ee81f387 100644 --- a/t/integration/leave_request/leave_request_revoke.js +++ b/t/integration/leave_request/leave_request_revoke.js @@ -174,10 +174,10 @@ describe('Revoke leave request', function(){ value : "2", },{ selector : 'input#from', - value : '2017-06-15', + value : '2018-05-15', },{ selector : 'input#to', - value : '2017-06-16', + value : '2018-05-16', }], message : /New leave request was added/, }) @@ -188,8 +188,8 @@ describe('Revoke leave request', function(){ it("Check that all days are marked as pended", function(done){ check_booking_func({ driver : driver, - full_days : [moment('2017-06-16')], - halfs_1st_days : [moment('2017-06-15')], + full_days : [moment('2018-05-16')], + halfs_1st_days : [moment('2018-05-15')], type : 'pended', }) .then(function(){ done() }); diff --git a/t/integration/leave_request/leave_request_revoke_by_admin.js b/t/integration/leave_request/leave_request_revoke_by_admin.js index b146d199c..5281605bc 100644 --- a/t/integration/leave_request/leave_request_revoke_by_admin.js +++ b/t/integration/leave_request/leave_request_revoke_by_admin.js @@ -113,10 +113,10 @@ describe('Revoke leave request by Admin', function(){ value : "2", },{ selector : 'input#from', - value : '2017-06-15', + value : '2018-05-15', },{ selector : 'input#to', - value : '2017-06-16', + value : '2018-05-16', }], message : /New leave request was added/, }) @@ -127,8 +127,8 @@ describe('Revoke leave request by Admin', function(){ it("Check that all days are marked as pended", function(done){ check_booking_func({ driver : driver, - full_days : [moment('2017-06-16')], - halfs_1st_days : [moment('2017-06-15')], + full_days : [moment('2018-05-16')], + halfs_1st_days : [moment('2018-05-15')], type : 'pended', }) .then(function(){ done() }); diff --git a/t/integration/leave_type_limit_next_year.js b/t/integration/leave_type_limit_next_year.js index 7a12f240c..68584b0d5 100644 --- a/t/integration/leave_type_limit_next_year.js +++ b/t/integration/leave_type_limit_next_year.js @@ -221,10 +221,10 @@ describe('Leave type limits for next year: ' + next_year, function(){ driver : driver, form_params : [{ selector : 'input#from', - value : next_year + '-05-11', + value : next_year + '-05-14', },{ selector : 'input#to', - value : next_year + '-05-11', + value : next_year + '-05-14', }], message : /Failed to create a leave request/, }) diff --git a/t/unit/model/db/user.js b/t/unit/model/db/user.js index e5ecd6564..bafa2483e 100644 --- a/t/unit/model/db/user.js +++ b/t/unit/model/db/user.js @@ -51,14 +51,14 @@ describe('get_automatic_adjustment method', function(){ describe('Start date is in current year, no end date', function(){ var employee = model.User.build({ - start_date : moment('2017-04-01'), + start_date : moment('2018-04-01'), }); employee.department = { allowance : 20, }; it('adjustment is made based on start date', function(){ - expect(employee.get_automatic_adjustment({now : moment('2017-07-20')})) + expect(employee.get_automatic_adjustment({now : moment('2018-07-20')})) .to.be.equal(-5); }); @@ -81,22 +81,22 @@ describe('get_automatic_adjustment method', function(){ describe('Start date is in current year, end date is in next year', function(){ var employee = model.User.build({ - start_date : moment('2017-04-01'), - end_date : moment('2018-10-01'), + start_date : moment('2018-04-01'), + end_date : moment('2019-10-01'), }); employee.department = { allowance : 20, }; it('adjustment is made based on start date', function(){ - expect(employee.get_automatic_adjustment({now : moment('2017-07-20')})) + expect(employee.get_automatic_adjustment({now : moment('2018-07-20')})) .to.be.equal(-5); }); }); describe('Start date is in next year, no end date', function(){ var employee = model.User.build({ - start_date : moment('2017-04-01'), + start_date : moment('2018-04-01'), }); employee.department = { allowance : 20, From 43153ad4e0016323d2258c5ade20c7f3a4709d80 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 2 Jan 2018 20:14:03 +0000 Subject: [PATCH 052/539] Add missing migration --- migrations/20180102-company-mode.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 migrations/20180102-company-mode.js diff --git a/migrations/20180102-company-mode.js b/migrations/20180102-company-mode.js new file mode 100644 index 000000000..5cb5c6f80 --- /dev/null +++ b/migrations/20180102-company-mode.js @@ -0,0 +1,27 @@ + +'use strict'; + +var models = require('../lib/model/db'); + +module.exports = { + up: function (queryInterface, Sequelize) { + + queryInterface.describeTable('Companies').then(function(attributes){ + + if (attributes.hasOwnProperty('mode')) { + return 1; + } + + return queryInterface.addColumn( + 'Companies', + 'mode', + models.Company.attributes.mode + ); + }); + + }, + + down: function (queryInterface, Sequelize) { + return queryInterface.removeColumn('Companies', 'mode'); + } +}; From ba0a66b5d0080232b459ffc45f02d77698dfdf24 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 2 Jan 2018 20:46:39 +0000 Subject: [PATCH 053/539] Update current year in footer --- views/partials/footer.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/partials/footer.hbs b/views/partials/footer.hbs index 44cbf4540..37e9afaed 100644 --- a/views/partials/footer.hbs +++ b/views/partials/footer.hbs @@ -6,7 +6,7 @@

    - © TimeOff.management 2017 + © TimeOff.management 2018 From 36e2116c92ce0ffd89fe454f778b5169e9426287 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 28 Dec 2017 14:24:05 +0000 Subject: [PATCH 054/539] Dates that system works on are in UTC --- lib/model/calendar_month.js | 8 +-- lib/model/db/bank_holiday.js | 2 +- lib/model/db/company.js | 2 +- lib/model/db/department.js | 3 +- lib/model/db/leave.js | 14 ++--- lib/model/db/schedule.js | 2 +- lib/model/db/user.js | 6 +- lib/model/db/user_allowance_adjustment.js | 2 +- lib/model/leave_collection.js | 2 +- lib/model/leave_day.js | 2 +- lib/model/leave_request_parameters.js | 10 ++-- lib/model/mixin/user/absence_aware.js | 68 +++++++++++------------ lib/model/mixin/user/company_aware.js | 10 ++-- lib/model/team_view.js | 2 +- lib/route/audit.js | 4 +- lib/route/calendar.js | 18 +++--- lib/route/feed.js | 6 +- lib/route/settings.js | 2 +- lib/route/users.js | 18 +++--- lib/view/helpers.js | 2 +- 20 files changed, 92 insertions(+), 91 deletions(-) diff --git a/lib/model/calendar_month.js b/lib/model/calendar_month.js index 85b9a9fa1..d944356c1 100644 --- a/lib/model/calendar_month.js +++ b/lib/model/calendar_month.js @@ -6,7 +6,7 @@ var moment = require('moment'), function CalendarMonth(day, args){ var me = this; - this.date = moment(day).startOf('month'); + this.date = moment.utc(day).startOf('month'); this._leaves = {}; this._bank_holidays = {}; @@ -21,7 +21,7 @@ function CalendarMonth(day, args){ if (args && args.bank_holidays){ var map = {}; args.bank_holidays.forEach(function(day){ - day = moment(day); + day = moment.utc(day); map[day.format(me.default_date_format())] = day; }); this._bank_holidays = map; @@ -30,7 +30,7 @@ function CalendarMonth(day, args){ if (args && args.leave_days){ var map = {}; args.leave_days.forEach(function(day){ - var attribute = moment(day.date).format(me.default_date_format()); + var attribute = moment.utc(day.date).format(me.default_date_format()); if ( ! map[attribute] ) { map[attribute] = day; } else if ( map[attribute] ) { @@ -146,7 +146,7 @@ CalendarMonth.prototype.is_weekend = function(day){ CalendarMonth.prototype.is_current_day = function(day){ return this.get_base_date().date(day).format( this.default_date_format() ) === - moment().format( this.default_date_format() ); + moment.utc().format( this.default_date_format() ); }; CalendarMonth.prototype.as_for_template = function(){ diff --git a/lib/model/db/bank_holiday.js b/lib/model/db/bank_holiday.js index b4249bc30..46a20203f 100644 --- a/lib/model/db/bank_holiday.js +++ b/lib/model/db/bank_holiday.js @@ -63,7 +63,7 @@ module.exports = function(sequelize, DataTypes) { instanceMethods : { get_pretty_date : function(){ - return moment(this.date).format('YYYY-MM-DD'); + return moment.utc(this.date).format('YYYY-MM-DD'); }, } }); diff --git a/lib/model/db/company.js b/lib/model/db/company.js index 2d17e6cf2..e896b19d9 100644 --- a/lib/model/db/company.js +++ b/lib/model/db/company.js @@ -387,7 +387,7 @@ module.exports = function(sequelize, DataTypes) { // Takes date string in format specific for current company and produce string // with date in generic format used internally within application normalise_date : function(date_str) { - return moment(date_str, this.get_default_date_format()).format('YYYY-MM-DD'); + return moment.utc(date_str, this.get_default_date_format()).format('YYYY-MM-DD'); }, // Promise schedule object valid for current company, if it does not have such diff --git a/lib/model/db/department.js b/lib/model/db/department.js index 316f48f19..27d592c0d 100644 --- a/lib/model/db/department.js +++ b/lib/model/db/department.js @@ -99,7 +99,8 @@ module.exports = function(sequelize, DataTypes) { var self = this, model = sequelize.models, - base_date = args.base_date || moment(); + // NOTE VPP: following date is going to be amended by user's time zone + base_date = args.base_date || moment.utc(); var promise_users_and_leaves = Promise.try(function(){ return self.promise_active_users(); diff --git a/lib/model/db/leave.js b/lib/model/db/leave.js index bbcd55e50..b19d4bdf6 100644 --- a/lib/model/db/leave.js +++ b/lib/model/db/leave.js @@ -129,8 +129,8 @@ module.exports = function(sequelize, DataTypes) { .then(() => employee.promise_boss()) .then(function(main_supervisor){ - var start_date = moment(valide_attributes.from_date), - end_date = moment(valide_attributes.to_date); + var start_date = moment.utc(valide_attributes.from_date), + end_date = moment.utc(valide_attributes.to_date); // Check that start date is not bigger then end one if ( start_date.toDate() > end_date.toDate() ) { @@ -175,8 +175,8 @@ module.exports = function(sequelize, DataTypes) { get_days : function() { var self = this, - start_date = moment(this.date_start), - end_date = moment(this.date_end), + start_date = moment.utc(this.date_start), + end_date = moment.utc(this.date_end), days = [ start_date ]; if (self.hasOwnProperty('_days')) { @@ -303,7 +303,7 @@ get_deducted_days : function(args) { } if (args && args.hasOwnProperty('year')) { - year = moment(args.year, 'YYYY'); + year = moment.utc(args.year, 'YYYY'); } // If current Leave stands for type that does not use @@ -331,10 +331,10 @@ get_deducted_days : function(args) { // If it happenned that current leave day is from the year current // call was made of, ignore that day - if (year && year.year() !== moment(leave_day.date).year()) return; + if (year && year.year() !== moment.utc(leave_day.date).year()) return; // Ignore non-working days (weekends) - if ( ! schedule.is_it_working_day({ day : moment(leave_day.date) }) ){ + if ( ! schedule.is_it_working_day({ day : moment.utc(leave_day.date) }) ){ return; } diff --git a/lib/model/db/schedule.js b/lib/model/db/schedule.js index 3e38f55eb..fc371f851 100644 --- a/lib/model/db/schedule.js +++ b/lib/model/db/schedule.js @@ -120,7 +120,7 @@ module.exports = function(sequelize, DataTypes){ throw new Error('"is_it_working_day" requires to have "day" parameter'); } - return this[ moment(day).format('dddd').toLowerCase() ] === works_whole_day(); + return this[ moment.utc(day).format('dddd').toLowerCase() ] === works_whole_day(); }, works_monday : function(){ diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 8240184c3..5cc936dd4 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -243,7 +243,7 @@ function get_instance_methods(sequelize) { let self = this; if ( ! year ){ - year = moment().format('YYYY'); + year = moment.utc().format('YYYY'); } // To avoid redundant trips to DB check cached value first @@ -282,7 +282,7 @@ function get_instance_methods(sequelize) { }, reload_with_leave_details : function(args){ - var year = args.year || moment(), + var year = args.year || moment.utc(), self = this; return Promise.join( @@ -561,7 +561,7 @@ function get_class_methods(sequelize) { return { $or : [ { end_date : {$eq : null}}, - { end_date : {$gte : moment().startOf('day').format('YYYY-MM-DD') }}, + { end_date : {$gte : moment.utc().startOf('day').format('YYYY-MM-DD') }}, ], }; }, diff --git a/lib/model/db/user_allowance_adjustment.js b/lib/model/db/user_allowance_adjustment.js index 4e35d890a..3ead47679 100644 --- a/lib/model/db/user_allowance_adjustment.js +++ b/lib/model/db/user_allowance_adjustment.js @@ -8,7 +8,7 @@ module.exports = function(sequelize, DataTypes){ year : { type : DataTypes.INTEGER, allowNull : false, - defaultValue : moment().format('YYYY'), + defaultValue : moment.utc().format('YYYY'), comment : 'Year when adjustment is applied', }, adjustment : { diff --git a/lib/model/leave_collection.js b/lib/model/leave_collection.js index 055e9e7fe..ecc9b9d61 100644 --- a/lib/model/leave_collection.js +++ b/lib/model/leave_collection.js @@ -17,7 +17,7 @@ function promise_to_group_leaves(leaves) { // Group leaves by years leaves.forEach(leave => { - let year = moment(leave.get_start_leave_day().date).format('YYYY'); + let year = moment.utc(leave.get_start_leave_day().date).format('YYYY'); if ( ! grouped_leaves[year]) { grouped_leaves[ year ] = { diff --git a/lib/model/leave_day.js b/lib/model/leave_day.js index 393822894..9ab90b286 100644 --- a/lib/model/leave_day.js +++ b/lib/model/leave_day.js @@ -54,7 +54,7 @@ LeaveDay.prototype.pretend_to_be_full_day = function() { }; LeaveDay.prototype.get_pretty_date = function(){ - return moment(this.date).format('YYYY-MM-DD'); + return moment.utc(this.date).format('YYYY-MM-DD'); }; module.exports = LeaveDay; diff --git a/lib/model/leave_request_parameters.js b/lib/model/leave_request_parameters.js index dfa0d93c9..1fde7796d 100644 --- a/lib/model/leave_request_parameters.js +++ b/lib/model/leave_request_parameters.js @@ -23,7 +23,7 @@ function LeaveRequest(args) { ); // From date should not be bigger then to - if (moment(args.from_date).toDate() > moment(args.to_date).toDate()){ + if (moment.utc(args.from_date).toDate() > moment.utc(args.to_date).toDate()){ throw new Error( 'From date should be before To date at LeaveRequest constructor' ); } @@ -52,9 +52,9 @@ LeaveRequest.prototype.as_data_object = function(){ }; LeaveRequest.prototype.is_within_one_day = function(){ - return moment(this.from_date).format('YYYY-MM-DD') + return moment.utc(this.from_date).format('YYYY-MM-DD') === - moment(this.to_date).format('YYYY-MM-DD'); + moment.utc(this.to_date).format('YYYY-MM-DD'); }; LeaveRequest.prototype._does_fit_with_point = function(leave_day, point_name){ @@ -62,9 +62,9 @@ LeaveRequest.prototype._does_fit_with_point = function(leave_day, point_name){ if ( ( - moment(leave_day.date).format('YYYY-MM-DD') + moment.utc(leave_day.date).format('YYYY-MM-DD') === - moment(this[point_name]).format('YYYY-MM-DD') + moment.utc(this[point_name]).format('YYYY-MM-DD') ) && (! leave_day.is_all_day_leave()) diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index d4f10ba47..2433cb415 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -22,23 +22,23 @@ module.exports = function(sequelize){ if (show_full_year) { return _.map([1,2,3,4,5,6,7,8,9,10,11,12], function(i){ - return moment(year.format('YYYY')+'-'+i+'-01'); + return moment.utc(year.format('YYYY')+'-'+i+'-01'); }); } return _.map([0,1,2,3], function(delta){ - return moment().add(delta, 'months').startOf('month'); + return moment.utc().add(delta, 'months').startOf('month'); }) }; this.promise_calendar = function(args) { - var year = args.year || moment(), + var year = args.year || moment.utc(), show_full_year = args.show_full_year || false, model = sequelize.models, this_user = this, // Find or if we need to show multi year calendar - is_multi_year = moment().month() > 8; + is_multi_year = moment.utc().month() > 8; var months_to_show = this_user._get_calendar_months_to_show({ year : year.clone(), @@ -70,16 +70,16 @@ module.exports = function(sequelize){ $or : { date_start : { $between : [ - moment(year).startOf('year').format('YYYY-MM-DD'), - moment( + moment.utc(year).startOf('year').format('YYYY-MM-DD'), + moment.utc( year.clone().add((is_multi_year ? 1 : 0), 'years') ).endOf('year').format('YYYY-MM-DD'), ] }, date_end : { $between : [ - moment( year ).startOf('year').format('YYYY-MM-DD'), - moment( + moment.utc( year ).startOf('year').format('YYYY-MM-DD'), + moment.utc( year.clone().add((is_multi_year ? 1 : 0), 'years') ).endOf('year').format('YYYY-MM-DD'), ] @@ -131,7 +131,7 @@ module.exports = function(sequelize){ var days_filter = { $between : [ new_leave_attributes.from_date, - moment(new_leave_attributes.to_date) + moment.utc(new_leave_attributes.to_date) .add(1,'days').format('YYYY-MM-DD'), ], }; @@ -181,7 +181,7 @@ module.exports = function(sequelize){ var self = this, where_clause = {}, - year = args.year || moment(); + year = args.year || moment.utc(); if (args && args.filter_status) { where_clause = { status : args.filter_status }; @@ -192,14 +192,14 @@ module.exports = function(sequelize){ where_clause['$or'] = { date_start : { $between : [ - moment(year).startOf('year').format('YYYY-MM-DD'), - moment(year).endOf('year').format('YYYY-MM-DD'), + moment.utc(year).startOf('year').format('YYYY-MM-DD'), + moment.utc(year).endOf('year').format('YYYY-MM-DD'), ] }, date_end : { $between : [ - moment(year).startOf('year').format('YYYY-MM-DD'), - moment(year).endOf('year').format('YYYY-MM-DD'), + moment.utc(year).startOf('year').format('YYYY-MM-DD'), + moment.utc(year).endOf('year').format('YYYY-MM-DD'), ] } }; @@ -253,7 +253,7 @@ module.exports = function(sequelize){ this.promise_my_active_leaves = function(args) { - var year = args.year || moment(); + var year = args.year || moment.utc(); return this.promise_my_leaves({ year : year, @@ -438,21 +438,21 @@ module.exports = function(sequelize){ this.get_automatic_adjustment = function(args) { - var now = (args && args.now) ? moment(args.now) : moment(); + var now = (args && args.now) ? moment.utc(args.now) : moment.utc(); if ( - now.year() !== moment(this.start_date).year() - && ( ! this.end_date || moment(this.end_date).year() > now.year() ) + now.year() !== moment.utc(this.start_date).year() + && ( ! this.end_date || moment.utc(this.end_date).year() > now.year() ) ){ return 0; } - var start_date = moment(this.start_date).year() === now.year() - ? moment(this.start_date) + var start_date = moment.utc(this.start_date).year() === now.year() + ? moment.utc(this.start_date) : now.startOf('year'), - end_date = this.end_date && moment(this.end_date).year() <= now.year() - ? moment(this.end_date) - : moment().endOf('year'); + end_date = this.end_date && moment.utc(this.end_date).year() <= now.year() + ? moment.utc(this.end_date) + : moment.utc().endOf('year'); return -1*(this.department.allowance - Math.round( this.department.allowance * end_date.diff(start_date, 'days') / 365 @@ -470,7 +470,7 @@ module.exports = function(sequelize){ // for that year, and if it is something other then current year, // we use department nominal allowance plus employee's allowance // (ignoreing automatic allowance) - if (year && year != moment().year()) { + if (year && year != moment.utc().year()) { return self.department.allowance + carried_over_allowance + adjustment; @@ -516,8 +516,8 @@ module.exports = function(sequelize){ this.promise_adjustment_and_carry_over_for_year = function(year){ let self = this; - year = year || moment(); - year = moment(year).format('YYYY'); + year = year || moment.utc(); + year = moment.utc(year).format('YYYY'); return self .getAdjustments({ @@ -559,7 +559,7 @@ module.exports = function(sequelize){ this.promise_to_update_adjustment = function(args) { let self = this, - year = args.year || moment().format('YYYY'), + year = args.year || moment.utc().format('YYYY'), adjustment = args.adjustment; // Update related allowance adjustement record @@ -586,7 +586,7 @@ module.exports = function(sequelize){ this.promise_to_update_carried_over_allowance = function(args) { let self = this, - year = args.year || moment().format('YYYY'), + year = args.year || moment.utc().format('YYYY'), carried_over_allowance = args.carried_over_allowance; // Update related allowance adjustement record @@ -612,7 +612,7 @@ module.exports = function(sequelize){ this.promise_my_leaves_for_calendar = function(args){ - var year = args.year || moment(); + var year = args.year || moment.utc(); return this.getMy_leaves({ where : { @@ -625,14 +625,14 @@ module.exports = function(sequelize){ $or : { date_start : { $between : [ - moment(year).startOf('year').format('YYYY-MM-DD'), - moment(year).endOf('year').format('YYYY-MM-DD'), + moment.utc(year).startOf('year').format('YYYY-MM-DD'), + moment.utc(year).endOf('year').format('YYYY-MM-DD'), ] }, date_end : { $between : [ - moment(year).startOf('year').format('YYYY-MM-DD'), - moment(year).endOf('year').format('YYYY-MM-DD'), + moment.utc(year).startOf('year').format('YYYY-MM-DD'), + moment.utc(year).endOf('year').format('YYYY-MM-DD'), ] } } @@ -651,7 +651,7 @@ module.exports = function(sequelize){ leave_type = args.leave_type, leave = args.leave, // Derive year from Leave object - year = args.year || moment(leave.date_start); + year = args.year || moment.utc(leave.date_start); // Make sure object contain all necessary data for that check return self.reload_with_leave_details({ diff --git a/lib/model/mixin/user/company_aware.js b/lib/model/mixin/user/company_aware.js index 17d4ef0e0..dfebedf0b 100644 --- a/lib/model/mixin/user/company_aware.js +++ b/lib/model/mixin/user/company_aware.js @@ -27,7 +27,7 @@ module.exports = function(sequelize){ // this.get_company_for_user_details = function(args){ var user_id = args.user_id, - year = args.year || moment(), + year = args.year || moment.utc(), current_user = this; return this.getCompany({ @@ -48,14 +48,14 @@ module.exports = function(sequelize){ $or : { date_start : { $between : [ - moment().startOf('year').format('YYYY-MM-DD'), - moment().endOf('year').format('YYYY-MM-DD'), + moment.utc().startOf('year').format('YYYY-MM-DD'), + moment.utc().endOf('year').format('YYYY-MM-DD'), ] }, date_end : { $between : [ - moment().startOf('year').format('YYYY-MM-DD'), - moment().endOf('year').format('YYYY-MM-DD'), + moment.utc().startOf('year').format('YYYY-MM-DD'), + moment.utc().endOf('year').format('YYYY-MM-DD'), ] } } diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 53cbede9c..25eee0be3 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -10,7 +10,7 @@ const function TeamView(args) { var me = this; - this.base_date = args.base_date || moment(); + this.base_date = args.base_date || moment.utc(); this.user = args.user; } diff --git a/lib/route/audit.js b/lib/route/audit.js index 52d627d52..4591ecd27 100644 --- a/lib/route/audit.js +++ b/lib/route/audit.js @@ -80,11 +80,11 @@ router.get(/email/, function(req, res){ }; if (start_date) { - filter.start_date = moment(start_date).format(req.user.company.get_default_date_format()); + filter.start_date = moment.utc(start_date).format(req.user.company.get_default_date_format()); } if (end_date) { - filter.end_date = moment(end_date).format(req.user.company.get_default_date_format()); + filter.end_date = moment.utc(end_date).format(req.user.company.get_default_date_format()); } res.render('audit/emails', { diff --git a/lib/route/calendar.js b/lib/route/calendar.js index b8d48bc58..d1b522ee2 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -98,8 +98,8 @@ router.post('/bookleave/', function(req, res){ router.get('/', function(req, res) { var current_year = validator.isNumeric(req.param('year')) - ? moment(req.param('year'), 'YYYY') - : moment(); + ? moment.utc(req.param('year'), 'YYYY') + : moment.utc(); var show_full_year = validator.toBoolean(req.param('show_full_year')); @@ -115,7 +115,7 @@ router.get('/', function(req, res) { function(calendar, company, user, supervisors, total_number_of_days_in_allowance){ let full_leave_type_statistics = user.get_leave_statistics_by_types(), - this_year = moment().format('YYYY'); + this_year = moment.utc().format('YYYY'); user .promise_number_of_days_available_in_allowance( this_year ) @@ -126,9 +126,9 @@ router.get('/', function(req, res) { title : 'Calendar', current_user : user, supervisors : supervisors, - previous_year : moment(current_year).add(-1,'year').format('YYYY'), + previous_year : moment.utc(current_year).add(-1,'year').format('YYYY'), current_year : current_year.format('YYYY'), - next_year : moment(current_year).add(1,'year').format('YYYY'), + next_year : moment.utc(current_year).add(1,'year').format('YYYY'), show_full_year : show_full_year, leave_type_statistics : _.filter(full_leave_type_statistics, st => st.days_taken > 0), full_leave_type_statistics : full_leave_type_statistics, @@ -145,8 +145,8 @@ router.get('/', function(req, res) { router.get('/teamview/', function(req, res){ var base_date = validator.isDate(req.param('date')) - ? moment(req.param('date')) - : moment(); + ? moment.utc(req.param('date')) + : moment.utc(); var team_view = new TeamView({ base_date : base_date, @@ -175,8 +175,8 @@ router.get('/teamview/', function(req, res){ })) .then(team_view_details => res.render('team_view', { base_date : base_date, - prev_date : moment(base_date).add(-1,'month'), - next_date : moment(base_date).add(1,'month'), + prev_date : moment.utc(base_date).add(-1,'month'), + next_date : moment.utc(base_date).add(1,'month'), users_and_leaves : team_view_details.users_and_leaves, related_departments : team_view_details.related_departments, current_department : team_view_details.current_department, diff --git a/lib/route/feed.js b/lib/route/feed.js index ff3908584..03f4522be 100644 --- a/lib/route/feed.js +++ b/lib/route/feed.js @@ -44,7 +44,7 @@ router.get('/:token/ical.ics', function(req, res){ if (feed.is_calendar()){ return user.promise_calendar({ - year : moment(), + year : moment.utc(), show_full_year : true, }) .then(function(calendar){ @@ -93,8 +93,8 @@ router.get('/:token/ical.ics', function(req, res){ return; } - var start = moment(day.moment), - end = moment(day.moment); + var start = moment.utc(day.moment), + end = moment.utc(day.moment); if (day.is_leave_morning && day.is_leave_afternoon) { start.hour(9).minute(0); diff --git a/lib/route/settings.js b/lib/route/settings.js index 493ba9565..0a3609da7 100644 --- a/lib/route/settings.js +++ b/lib/route/settings.js @@ -476,7 +476,7 @@ router.post('/company/export/', function(req, res){ .then(function(company){ res.attachment( - company.name_for_machine()+'_'+moment().format('YYYYMMDD-hhmmss')+'.json' + company.name_for_machine()+'_'+moment.utc().format('YYYYMMDD-hhmmss')+'.json' ); res.send(JSON.stringify(company)); diff --git a/lib/route/users.js b/lib/route/users.js index ca351a5b5..1dece7612 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -165,10 +165,10 @@ router.get('/edit/:user_id/absences/', function(req, res){ .promise_total_number_of_days_in_allowance(), employee - .promise_adjustmet_for_year(moment().format('YYYY')), + .promise_adjustmet_for_year(moment.utc().format('YYYY')), employee - .promise_carried_over_allowance_for_year(moment().format('YYYY')), + .promise_carried_over_allowance_for_year(moment.utc().format('YYYY')), (args, total_days_number, employee_adjustment, carried_over_allowance) => { args.push(total_days_number); @@ -291,7 +291,7 @@ var ensure_user_was_not_useed_elsewhere_while_being_inactive = function(args){ // new "end_date" is provided // new "end_date" is in future new_user_attributes.end_date && - moment( new_user_attributes.end_date ).startOf('day').toDate() >= moment().startOf('day').toDate() + moment.utc( new_user_attributes.end_date ).startOf('day').toDate() >= moment.utc().startOf('day').toDate() ) ) ) { @@ -443,7 +443,7 @@ router.post('/edit/:user_id/', function(req, res){ .then(() => { if ( adjustment !== undefined ) { return employee.promise_to_update_adjustment({ - year : moment().format('YYYY'), + year : moment.utc().format('YYYY'), adjustment : adjustment, }); } @@ -590,14 +590,14 @@ router.get('/', function(req, res) { $or : { date_start : { $between : [ - moment().startOf('year').format('YYYY-MM-DD'), - moment().endOf('year').format('YYYY-MM-DD'), + moment.utc().startOf('year').format('YYYY-MM-DD'), + moment.utc().endOf('year').format('YYYY-MM-DD'), ] }, date_end : { $between : [ - moment().startOf('year').format('YYYY-MM-DD'), - moment().endOf('year').format('YYYY-MM-DD'), + moment.utc().startOf('year').format('YYYY-MM-DD'), + moment.utc().endOf('year').format('YYYY-MM-DD'), ] } } @@ -762,7 +762,7 @@ function get_and_validate_user_parameters(args) { if ( start_date && end_date && - moment(start_date).toDate() > moment(end_date).toDate() + moment.utc(start_date).toDate() > moment.utc(end_date).toDate() ){ req.session.flash_error( 'End date for '+item_name+' is before start date' diff --git a/lib/view/helpers.js b/lib/view/helpers.js index b8680b433..efcf06f09 100644 --- a/lib/view/helpers.js +++ b/lib/view/helpers.js @@ -13,7 +13,7 @@ var as_date_formatted = function(date_str, format, options) { format = get_current_format( options ); } - return moment(date_str).format(format); + return moment.utc(date_str).format(format); } // Note: that currently it returns only truely value of given property, that is if there property From 71dc373bd8ba43212efa67f191434ddfb734d370 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 28 Dec 2017 16:00:12 +0000 Subject: [PATCH 055/539] Add timezone field to company and UI to edit it --- lib/model/db/company.js | 6 ++++ lib/route/settings.js | 8 ++++++ ...20171228-add-time-zone-field-to-company.js | 28 +++++++++++++++++++ package.json | 4 +-- views/general_settings.hbs | 14 ++++++++++ 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 migrations/20171228-add-time-zone-field-to-company.js diff --git a/lib/model/db/company.js b/lib/model/db/company.js index e896b19d9..8fc437b35 100644 --- a/lib/model/db/company.js +++ b/lib/model/db/company.js @@ -72,6 +72,12 @@ module.exports = function(sequelize, DataTypes) { defaultValue : 1, comment : "Indicate which mode the company account is in.", }, + timezone : { + type : DataTypes.TEXT, + allowNull : true, + defaultValue : 'Europe/London', + comment : 'Timezone current company is located in', + }, }, { indexes : [ diff --git a/lib/route/settings.js b/lib/route/settings.js index 0a3609da7..f198319e9 100644 --- a/lib/route/settings.js +++ b/lib/route/settings.js @@ -9,6 +9,7 @@ var express = require('express'), validator = require('validator'), Promise = require('bluebird'), moment = require('moment'), + moment_tz = require('moment-timezone'), config = require('../config'), _ = require('underscore'); @@ -48,6 +49,7 @@ router.get('/general/', function(req, res){ company : company, schedule : schedule, countries : config.get('countries'), + timezones_available : moment_tz.tz.names(), }); }); }); @@ -57,6 +59,7 @@ router.post('/company/', function(req, res){ var name = validator.trim(req.param('name')), country_code = validator.trim(req.param('country')), date_format = validator.trim(req.param('date_format')), + timezone = validator.trim(req.param('timezone')), share_all_absences= validator.toBoolean( req.param('share_all_absences') ); @@ -65,6 +68,10 @@ router.post('/company/', function(req, res){ req.session.flash_error('Country should contain only letters and numbers'); } + if ( ! moment_tz.tz.names().find(tz_str => tz_str === timezone) ) { + req.session.flash_error('Time zone is unknown'); + } + // In case of validation error redirect back to edit form if ( req.session.flash_has_errors() ) { return res.redirect_with_session('/settings/general/'); @@ -89,6 +96,7 @@ router.post('/company/', function(req, res){ company.country = country_code; company.share_all_absences= share_all_absences; company.date_format = date_format; + company.timezone = timezone; return company.save(); }) diff --git a/migrations/20171228-add-time-zone-field-to-company.js b/migrations/20171228-add-time-zone-field-to-company.js new file mode 100644 index 000000000..f6236f614 --- /dev/null +++ b/migrations/20171228-add-time-zone-field-to-company.js @@ -0,0 +1,28 @@ + +'use strict'; + +var models = require('../lib/model/db'); + +module.exports = { + up: function (queryInterface, Sequelize) { + + return queryInterface.describeTable('Companies') + .then(function(attributes){ + + if (attributes.hasOwnProperty('timezone')) { + return 1; + } + + return queryInterface.addColumn( + 'Companies', + 'timezone', + models.Company.attributes.timezone + ); + }); + }, + + down: function (queryInterface, Sequelize) { + return queryInterface + .removeColumn('Companies', 'timezone'); + } +}; diff --git a/package.json b/package.json index d9ca48a01..f38695cf6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "joi": "~12.0.0", "ldapauth-fork": "^2.5.2", "moment": "^2.11.2", - "moment-timezone": "^0.5.10", + "moment-timezone": "^0.5.14", "morgan": "^1.3.2", "nconf": "^0.8.4", "node-uuid": "^1.4.7", @@ -47,7 +47,7 @@ "test": "node node_modules/mocha/bin/mocha --recursive t/*", "start": "node bin/wwww", "db-update": "node node_modules/.bin/sequelize db:migrate --config=config/db.json --models-path=lib/model/db/", - "carry-over-allowance" : "node bin/calculate_carry_over_allowance_for_all_users.js", + "carry-over-allowance": "node bin/calculate_carry_over_allowance_for_all_users.js", "compile-sass": "node node_modules/node-sass/bin/node-sass scss/main.scss public/css/style.css" } } diff --git a/views/general_settings.hbs b/views/general_settings.hbs index c8641bd9b..c61959880 100644 --- a/views/general_settings.hbs +++ b/views/general_settings.hbs @@ -49,6 +49,20 @@ + + +

    + +
    + +
    +
    + +
    From 838aa5d7fcec8cd627ad4c3f581d02dc9e72f4c5 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 28 Dec 2017 18:28:00 +0000 Subject: [PATCH 056/539] Start and end booking dates are TZ aware --- app.js | 29 +++++++++++++++++++++-------- lib/route/calendar.js | 1 - 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/app.js b/app.js index de753b088..1bbf71eac 100644 --- a/app.js +++ b/app.js @@ -57,14 +57,27 @@ app.use(passport.session()); // // Make sure session and user objects are available in templates app.use(function(req,res,next){ - res.locals.session = req.session; - res.locals.logged_user = req.user; - res.locals.url_to_the_site_root = '/'; - res.locals.requested_path = req.originalUrl; - // For book leave request modal - res.locals.booking_start = moment(); - res.locals.booking_end = moment(); - next(); + + // Get today given user's timezone + var today; + + if ( req.user && req.user.company ) { + today = moment.utc( + moment.utc().tz(req.user.company.timezone).format('YYYY-MM-DD') + ); + } else { + today = moment.utc(); + } + + res.locals.session = req.session; + res.locals.logged_user = req.user; + res.locals.url_to_the_site_root = '/'; + res.locals.requested_path = req.originalUrl; + // For book leave request modal + res.locals.booking_start = today, + res.locals.booking_end = today, + + next(); }); app.use(function(req,res,next){ diff --git a/lib/route/calendar.js b/lib/route/calendar.js index d1b522ee2..c0fe7eee2 100644 --- a/lib/route/calendar.js +++ b/lib/route/calendar.js @@ -9,7 +9,6 @@ var express = require('express'), validator = require('validator'), get_and_validate_leave_params = require('./validator/leave_request'), TeamView = require('../model/team_view'), - CalendarMonth = require('../model/calendar_month'), EmailTransport = require('../email'); router.post('/bookleave/', function(req, res){ From 31549d3e797eed7c52c1c1f4ffca10910253a654 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 28 Dec 2017 19:12:25 +0000 Subject: [PATCH 057/539] Add view helpers for TZ aware date formatting --- app.js | 4 +-- lib/model/db/company.js | 11 ++++++++ lib/view/helpers.js | 57 ++++++++++++++++++++++++++++++++++++++--- views/audit/emails.hbs | 2 +- views/requests.hbs | 2 +- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/app.js b/app.js index 1bbf71eac..c0155a27d 100644 --- a/app.js +++ b/app.js @@ -62,9 +62,7 @@ app.use(function(req,res,next){ var today; if ( req.user && req.user.company ) { - today = moment.utc( - moment.utc().tz(req.user.company.timezone).format('YYYY-MM-DD') - ); + today = req.user.company.get_today(); } else { today = moment.utc(); } diff --git a/lib/model/db/company.js b/lib/model/db/company.js index 8fc437b35..d9c6fa77c 100644 --- a/lib/model/db/company.js +++ b/lib/model/db/company.js @@ -3,6 +3,7 @@ var Promise = require("bluebird"), LdapAuth = require('ldapauth-fork'), moment = require('moment'), + moment_tz = require('moment-timezone'), _ = require('underscore'); module.exports = function(sequelize, DataTypes) { @@ -396,6 +397,16 @@ module.exports = function(sequelize, DataTypes) { return moment.utc(date_str, this.get_default_date_format()).format('YYYY-MM-DD'); }, + // Returns moment UTC-ed object that takes into consideration company time zone + // (p to day's precision) + get_today : function() { + let self = this; + + return moment.utc( + moment_tz.utc().tz(self.timezone).format('YYYY-MM-DD') + ); + }, + // Promise schedule object valid for current company, if it does not have such // in databse, retulr default one promise_schedule : function(){ diff --git a/lib/view/helpers.js b/lib/view/helpers.js index efcf06f09..3ee5d8708 100644 --- a/lib/view/helpers.js +++ b/lib/view/helpers.js @@ -2,8 +2,9 @@ "use strict"; var - moment = require('moment-timezone'), - config = require('../config'); + moment = require('moment'), + moment_tz = require('moment-timezone'), + config = require('../config'); var as_date_formatted = function(date_str, format, options) { @@ -13,6 +14,13 @@ var as_date_formatted = function(date_str, format, options) { format = get_current_format( options ); } + // Special case when we have to take time zone into consideration + // when printing date (usually it is needed for those dates recorded + // automatically as UTC time stampts) + if ( options.tom_take_timezone_into_consideration ) { + return moment_tz.utc(date_str).tz( get_timezone( options ) ).format( format ); + } + return moment.utc(date_str).format(format); } @@ -44,7 +52,21 @@ var get_current_format = function(options){ } return format; -} +}; + +var get_timezone = function(options) { + + let timezone = 'Europe/London'; + + var user = get_property_from_options(options, 'logged_user') + || get_property_from_options(options, 'user'); + + if ( user && user.hasOwnProperty('company') ) { + timezone = user.company.timezone; + } + + return timezone; +}; module.exports = function(){ return { @@ -72,6 +94,35 @@ module.exports = function(){ ); }, + // Do the same as "as_date" method but takes into consideration of company + // timezone. It is needed for rendering dates that were recorded in database + // automatically in UTC, dates that were recorded based on explicit input + // from users should not be used with this method (but with "as_date" instead) + as_date_from_timestamp : function(date_string, options){ + + // Add custom flag to options so further down the call stack + // we would know that date needs to be corrected with + // current timezone + options.tom_take_timezone_into_consideration = true; + + return as_date_formatted(date_string, undefined, options); + }, + + // Similar to "as_datetime" but with the same twist as "as_date_from_timestamp" + as_datetime_from_timestamp : function(date_str, options){ + + // Add custom flag to options so further down the call stack + // we would know that date needs to be corrected with + // current timezone + options.tom_take_timezone_into_consideration = true; + + return as_date_formatted( + date_str, + get_current_format(options) + ' HH:mm:ss', + options + ); + }, + // Given string with UTC date and string with moment.js format return corresponding string as_date_formatted : as_date_formatted, diff --git a/views/audit/emails.hbs b/views/audit/emails.hbs index aca3de23a..06d9c0cde 100644 --- a/views/audit/emails.hbs +++ b/views/audit/emails.hbs @@ -77,7 +77,7 @@
    - {{as_datetime this.created_at}} + {{as_datetime_from_timestamp this.created_at}}
    {{#with this.user}}{{this.full_name}}{{/with}} {{this.user.department.name}}{{as_date this.createdAt}}{{as_date_from_timestamp this.createdAt}} From {{#with this.get_start_leave_day}}{{as_date this.date}}{{/with}} to {{#with this.get_end_leave_day}}{{as_date this.date}}{{/with}} {{#if this.is_pended_revoke_leave}}REVOKE {{/if}}{{this.leave_type.name}} {{ this.get_deducted_days_number }}
    {{#with this.user}}{{this.full_name}}{{/with}} {{this.user.department.name}}{{as_date_from_timestamp this.createdAt}}From {{#with this.get_start_leave_day}}{{as_date this.date}}{{/with}} to {{#with this.get_end_leave_day}}{{as_date this.date}}{{/with}}{{as_date_from_timestamp this.createdAt}}{{#with this.get_start_leave_day}}{{as_date this.date}}{{/with}} {{#with this.get_end_leave_day}}{{as_date this.date}}{{/with}} {{#if this.is_pended_revoke_leave}}REVOKE {{/if}}{{this.leave_type.name}} {{ this.get_deducted_days_number }} From 8ba837e9d9de823f984419142dd6c43d41b2154e Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 5 Jan 2018 06:30:03 +0000 Subject: [PATCH 062/539] Simple commit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af8d5262d..3fd6d000f 100644 --- a/README.md +++ b/README.md @@ -126,4 +126,5 @@ npm start ## Feedback -Please report any issues or feedback to twitter or Email: pavlo at timeoff.management \ No newline at end of file +Please report any issues or feedback to twitter or Email: pavlo at timeoff.management + From db52ced05288a532ac1b59dd6cdf27f795961d80 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 5 Jan 2018 06:48:24 +0000 Subject: [PATCH 063/539] Add sorting order column to leave type. Allow editing newly added flag. --- lib/model/db/leave_type.js | 6 + lib/model/mixin/user/company_aware.js | 1 + lib/route/settings.js | 155 +++++++++--------- .../20180103-sort-order-for-leave-types.js | 27 +++ t/integration/crud_leave_type.js | 45 +++-- t/integration/leave_type_limit_in_action.js | 2 +- t/integration/leave_type_limit_next_year.js | 2 +- t/integration/remove_used_leave_type.js | 2 +- views/general_settings.hbs | 27 ++- views/partials/add_new_leave_type_modal.hbs | 7 +- 10 files changed, 157 insertions(+), 117 deletions(-) create mode 100644 migrations/20180103-sort-order-for-leave-types.js diff --git a/lib/model/db/leave_type.js b/lib/model/db/leave_type.js index 322400b0c..9bef02bb2 100644 --- a/lib/model/db/leave_type.js +++ b/lib/model/db/leave_type.js @@ -22,6 +22,12 @@ module.exports = function(sequelize, DataTypes) { allowNull : false, defaultValue : 0, }, + sort_order : { + type : DataTypes.INTEGER, + allowNull : false, + defaultValue : 0, + comment : "Is used to determine sorting order of leave types", + }, }, { classMethods: { associate : function( models ) { diff --git a/lib/model/mixin/user/company_aware.js b/lib/model/mixin/user/company_aware.js index dfebedf0b..e77649d49 100644 --- a/lib/model/mixin/user/company_aware.js +++ b/lib/model/mixin/user/company_aware.js @@ -152,6 +152,7 @@ module.exports = function(sequelize){ as : 'leave_types', }], order : [ + [{ model : sequelize.models.LeaveType, as : 'leave_types' }, 'sort_order', 'DESC'], [{ model : sequelize.models.LeaveType, as : 'leave_types' }, 'name'] ] }); diff --git a/lib/route/settings.js b/lib/route/settings.js index f198319e9..bf660870b 100644 --- a/lib/route/settings.js +++ b/lib/route/settings.js @@ -331,86 +331,73 @@ router.post('/bankholidays/delete/:bank_holiday_number/', function(req, res){ router.post('/leavetypes', function(req, res){ - var model = req.app.get('db_model'); - - req.user.getCompany({ - include : [{ model : model.LeaveType, as : 'leave_types' }], - order : [[{model: model.LeaveType, as : 'leave_types'}, 'name' ]], - }) - .then(function(company){ + var model = req.app.get('db_model'); - var promise_new_leave_type = Promise.resolve(1); + req.user - if (validator.trim(req.param('name__new'))) { - var attributes = get_and_validate_leave_type({ - req : req, - suffix : 'new', - item_name : 'New Leave Type' - }); - if ( req.session.flash_has_errors() ) { - return Promise.resolve(1); - } - attributes.companyId = company.id; - promise_new_leave_type = model.LeaveType.create(attributes); - } + .get_company_with_all_leave_types() + .then(function(company){ - return Promise.all([ - promise_new_leave_type, - _.map( + var promise_new_leave_type = Promise.resolve(1); - company.leave_types, - function(leave_type, index){ + if (validator.trim(req.param('name__new'))) { + var attributes = get_and_validate_leave_type({ + req : req, + suffix : 'new', + item_name : 'New Leave Type' + }); + attributes.companyId = company.id; + promise_new_leave_type = model.LeaveType.create(attributes); + } - var attributes = get_and_validate_leave_type({ - req : req, - suffix : index, - item_name : leave_type.name, - }); + return Promise.all([ + promise_new_leave_type, + _.map( + company.leave_types, + function(leave_type, index){ - // If there were any validation errors: do not update leave type - // (it affects all leave types, that is if one failed - // validation - all leave types are not to be updated) - if ( req.session.flash_has_errors() ) { - return Promise.resolve(1); - } + var attributes = get_and_validate_leave_type({ + req : req, + suffix : leave_type.id, + item_name : leave_type.name, + }); - return leave_type.updateAttributes(attributes); - } + return leave_type.updateAttributes(attributes); + } - ) // End of map that create leave type update promises - ]); + ) // End of map that create leave type update promises + ]); }) - .then(function(){ - if ( ! req.session.flash_has_errors() ) { - req.session.flash_message('Changes to leave types were saved'); - } - return res.redirect_with_session('/settings/general/'); + .then(() => { + req.session.flash_message('Changes to leave types were saved'); + return res.redirect_with_session('/settings/general/'); }) - .catch(function(error){ - console.error( - 'An error occurred when trying to edit Leave types by user '+req.user.id - + ' : ' + error - ); + .catch(error => { + console.error( + 'An error occurred when trying to edit Leave types by user '+req.user.id + + ' : ' + error + ); - req.session.flash_error( - 'Failed to update leave types details, please contact customer service' - ); + if (error.hasOwnProperty('user_message')) { + req.session.flash_error( error.user_message ); + } - return res.redirect_with_session('/settings/general/'); - }); + req.session.flash_error( + 'Failed to update leave types details, please contact customer service' + ); + return res.redirect_with_session('/settings/general/'); + }); }); -router.post('/leavetypes/delete/:leave_type_number/', function(req, res){ +router.post('/leavetypes/delete/:leave_type_id/', function(req, res){ - // leave_type_number is an index number of leave_type to be removed based - // on the list of leave types on the page, this is not an ID - var leave_type_number = req.param('leave_type_number'); + var leave_type_id = req.param('leave_type_id'); var model = req.app.get('db_model'); - if (!validator.isInt(leave_type_number)) { + if (!validator.isInt(leave_type_id)) { console.error( 'User '+req.user.id+' submited non-int leave_type number ' +bank_holiday_number @@ -430,7 +417,7 @@ router.post('/leavetypes/delete/:leave_type_number/', function(req, res){ order : [[{model: model.LeaveType, as : 'leave_types'}, 'name' ]], }) .then(function(company){ - var leave_type_to_remove = company.leave_types[ leave_type_number ]; + var leave_type_to_remove = company.leave_types.find( lt => String(lt.id) === String(leave_type_id) ); // Check if user specify valid department number if (! leave_type_to_remove) { @@ -439,7 +426,7 @@ router.post('/leavetypes/delete/:leave_type_number/', function(req, res){ throw new Error( 'User '+req.user.id+' tried to remove non-existing leave type number' - +leave_type_number+' out of '+company.leave_types.length + +leave_type_id+' out of '+company.leave_types.length ); @@ -588,33 +575,38 @@ function get_and_validate_bank_holiday(args) { } function get_and_validate_leave_type(args) { - var req = args.req, - index = args.suffix, - item_name = args.item_name; + let + req = args.req, + suffix = args.suffix, + item_name = args.item_name; // Get user parameters - var name = validator.trim(req.param('name__'+index)), - color = validator.trim(req.param('color__'+index)) || '#f1f1f1', - limit = validator.trim(req.param('limit__'+index)) || 0, - use_allowance = validator.toBoolean( - req.param('use_allowance__'+index) - ); + let + name = validator.trim(req.param('name__'+suffix)), + color = validator.trim(req.param('color__'+suffix)) || '#f1f1f1', + limit = validator.trim(req.param('limit__'+suffix)) || 0, + first_record = validator.trim(req.param('first_record')) || 0, + use_allowance = validator.toBoolean( + req.param('use_allowance__'+suffix) + ); - // Validate provided parameters + // VPP TODO move that into resusable component + let throw_user_error = function(message){ + let error = new Error(message); + error.user_message = message; + throw error; + }; + // Validate provided parameters if ( ! validator.isHexColor(color)) { - req.session.flash_error( - 'New color for '+item_name+' should be color code' - ); + throw_user_error( 'New color for '+item_name+' should be color code' ); } + if ( ! validator.isNumeric(limit) ){ - req.session.flash_error( - 'New limit for '+item_name+' should be a valide number' - ); + throw_user_error( 'New limit for '+item_name+' should be a valide number' ); + } else if ( limit < 0) { - req.session.flash_error( - 'New limit for '+item_name+' should be positive number or 0' - ); + throw_user_error( 'New limit for '+item_name+' should be positive number or 0' ); } return { @@ -622,6 +614,7 @@ function get_and_validate_leave_type(args) { color : color, use_allowance : use_allowance, limit : limit, + sort_order : ( (first_record && (String(first_record)===String(suffix))? 1 : 0) ), }; } diff --git a/migrations/20180103-sort-order-for-leave-types.js b/migrations/20180103-sort-order-for-leave-types.js new file mode 100644 index 000000000..4899d3997 --- /dev/null +++ b/migrations/20180103-sort-order-for-leave-types.js @@ -0,0 +1,27 @@ + +'use strict'; + +var models = require('../lib/model/db'); + +module.exports = { + up: function (queryInterface, Sequelize) { + + queryInterface.describeTable('LeaveTypes').then(function(attributes){ + + if (attributes.hasOwnProperty('sort_order')) { + return 1; + } + + return queryInterface.addColumn( + 'LeaveTypes', + 'sort_order', + models.LeaveType.attributes.sort_order + ); + }); + + }, + + down: function (queryInterface, Sequelize) { + return queryInterface.removeColumn('LeaveTypes', 'sort_order'); + } +}; diff --git a/t/integration/crud_leave_type.js b/t/integration/crud_leave_type.js index c905386ae..bca2f3946 100644 --- a/t/integration/crud_leave_type.js +++ b/t/integration/crud_leave_type.js @@ -41,17 +41,16 @@ describe('CRUD for leave types', function(){ check_elements_func({ driver : driver, elements_to_check : [{ - selector : leave_type_edit_form_id+' input[name="name__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_0"]', value : 'Holiday', },{ - selector : leave_type_edit_form_id+' input[name="name__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_1"]', value : 'Sick Leave', }], }) .then(function(){ done() }); }); - // TODO un comment that step when we resurect editing colors // // Try to submit form with incorrect color code // .then(function(data){ @@ -70,11 +69,11 @@ describe('CRUD for leave types', function(){ check_elements_func({ driver : driver, elements_to_check : [{ - selector : leave_type_edit_form_id+' input[name="use_allowance__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="allowance_0"]', tick : true, value : 'on', },{ - selector : leave_type_edit_form_id+' input[name="use_allowance__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="allowance_1"]', tick : true, value : 'off', }], @@ -86,7 +85,7 @@ describe('CRUD for leave types', function(){ submit_form_func({ driver : driver, form_params : [{ - selector : leave_type_edit_form_id+' input[name="use_allowance__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="allowance_1"]', tick : true, value : 'on', }], @@ -101,11 +100,11 @@ describe('CRUD for leave types', function(){ check_elements_func({ driver : driver, elements_to_check : [{ - selector : leave_type_edit_form_id+' input[name="use_allowance__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="allowance_0"]', value : 'on', tick : true, },{ - selector : leave_type_edit_form_id+' input[name="use_allowance__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="allowance_1"]', value : 'on', tick : true, }], @@ -117,10 +116,10 @@ describe('CRUD for leave types', function(){ submit_form_func({ driver : driver, form_params : [{ - selector : leave_type_edit_form_id+' input[name="limit__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="limit_0"]', value : '0', },{ - selector : leave_type_edit_form_id+' input[name="limit__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="limit_1"]', value : '5', }], submit_button_selector : leave_type_edit_form_id+' button[type="submit"]', @@ -134,7 +133,7 @@ describe('CRUD for leave types', function(){ submit_form_func({ driver : driver, form_params : [{ - selector : leave_type_edit_form_id+' input[name="limit__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="limit_0"]', value : '-1', }], submit_button_selector : leave_type_edit_form_id+' button[type="submit"]', @@ -174,13 +173,13 @@ describe('CRUD for leave types', function(){ check_elements_func({ driver : driver, elements_to_check : [{ - selector : leave_type_new_form_id+' input[name="name__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_0"]', value : 'AAAAA', },{ - selector : leave_type_new_form_id+' input[name="name__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_1"]', value : 'Holiday', },{ - selector : leave_type_new_form_id+' input[name="name__2"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_2"]', value : 'Sick Leave', }], }) @@ -191,7 +190,7 @@ describe('CRUD for leave types', function(){ submit_form_func({ driver : driver, form_params : [{ - selector : leave_type_edit_form_id+' input[name="name__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_0"]', value : 'MM', }], submit_button_selector : leave_type_edit_form_id+' button[type="submit"]', @@ -204,13 +203,13 @@ describe('CRUD for leave types', function(){ check_elements_func({ driver : driver, elements_to_check : [{ - selector : leave_type_edit_form_id+' input[name="name__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_0"]', value : 'Holiday', },{ - selector : leave_type_edit_form_id+' input[name="name__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_1"]', value : 'MM', },{ - selector : leave_type_edit_form_id+' input[name="name__2"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_2"]', value : 'Sick Leave', }], }) @@ -221,13 +220,13 @@ describe('CRUD for leave types', function(){ submit_form_func({ driver : driver, elements_to_check : [{ - selector : leave_type_edit_form_id+' input[name="name__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_0"]', value : 'Holiday', },{ - selector : leave_type_edit_form_id+' input[name="name__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_1"]', value : 'Sick Leave', }], - submit_button_selector : leave_type_edit_form_id+' button[value="1"]', + submit_button_selector : leave_type_edit_form_id+' button[data-tom-leave-type-order="remove_1"]', message : /Leave type was successfully removed/, }) .then(function(){ done() }); @@ -237,10 +236,10 @@ describe('CRUD for leave types', function(){ check_elements_func({ driver : driver, elements_to_check : [{ - selector : leave_type_edit_form_id+' input[name="name__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_0"]', value : 'Holiday', },{ - selector : leave_type_edit_form_id+' input[name="name__1"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_1"]', value : 'Sick Leave', }], }) diff --git a/t/integration/leave_type_limit_in_action.js b/t/integration/leave_type_limit_in_action.js index ac465ab57..3746bdad7 100644 --- a/t/integration/leave_type_limit_in_action.js +++ b/t/integration/leave_type_limit_in_action.js @@ -60,7 +60,7 @@ describe('Leave type limits in actoion', function(){ submit_form_func({ driver : driver, form_params : [{ - selector : leave_type_edit_form_id+' input[name="limit__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="limit_0"]', value : '3', }], submit_button_selector : leave_type_edit_form_id+' button[type="submit"]', diff --git a/t/integration/leave_type_limit_next_year.js b/t/integration/leave_type_limit_next_year.js index 68584b0d5..848b83321 100644 --- a/t/integration/leave_type_limit_next_year.js +++ b/t/integration/leave_type_limit_next_year.js @@ -67,7 +67,7 @@ describe('Leave type limits for next year: ' + next_year, function(){ submit_form_func({ driver : driver, form_params : [{ - selector : leave_type_edit_form_id+' input[name="limit__0"]', + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="limit_0"]', value : '1', }], submit_button_selector : leave_type_edit_form_id+' button[type="submit"]', diff --git a/t/integration/remove_used_leave_type.js b/t/integration/remove_used_leave_type.js index d7024d262..ee53368c5 100644 --- a/t/integration/remove_used_leave_type.js +++ b/t/integration/remove_used_leave_type.js @@ -140,7 +140,7 @@ describe('Try to remove used leave type', function(){ it("Try to remove newly added leave type and ensure it fails", function(done){ submit_form_func({ driver : driver, - submit_button_selector : leave_type_edit_form_id+' button[value="0"]', + submit_button_selector : leave_type_edit_form_id+' button[data-tom-leave-type-order="remove_0"]', message : /Cannot remove leave type: type is in use/, }) .then(function(){ done() }); diff --git a/views/general_settings.hbs b/views/general_settings.hbs index c61959880..7c0969b31 100644 --- a/views/general_settings.hbs +++ b/views/general_settings.hbs @@ -206,8 +206,14 @@
    -
    -
    +
    + +

    Tick one to always be on top of the list

    +
    +
    + +

    Days/year

    +
     
    @@ -225,17 +231,24 @@ {{#each company.leave_types}}
    - + +
    + + + + +
    +
    - - + +
    - +
    - +
    diff --git a/views/partials/add_new_leave_type_modal.hbs b/views/partials/add_new_leave_type_modal.hbs index f92c2ffb4..18e34c097 100644 --- a/views/partials/add_new_leave_type_modal.hbs +++ b/views/partials/add_new_leave_type_modal.hbs @@ -12,9 +12,10 @@ {{#each company.leave_types}} - - - + + + + {{/each}} From 0912927b71e2eed32b2f6247854c81612a76ba81 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Sun, 7 Jan 2018 19:26:57 +0000 Subject: [PATCH 064/539] Add test for leave type orders --- lib/route/settings.js | 13 +- t/integration/crud_leave_type.js | 139 +++++++++++++++++++- views/partials/add_new_leave_type_modal.hbs | 9 -- views/partials/book_leave_modal.hbs | 2 +- 4 files changed, 150 insertions(+), 13 deletions(-) diff --git a/lib/route/settings.js b/lib/route/settings.js index bf660870b..ff46017ee 100644 --- a/lib/route/settings.js +++ b/lib/route/settings.js @@ -356,13 +356,16 @@ router.post('/leavetypes', function(req, res){ company.leave_types, function(leave_type, index){ - var attributes = get_and_validate_leave_type({ + let attributes = get_and_validate_leave_type({ req : req, suffix : leave_type.id, item_name : leave_type.name, }); - return leave_type.updateAttributes(attributes); + // Update leave type only if there are attributes submitted for it + return attributes + ? leave_type.updateAttributes(attributes) + : Promise.resolve(1); } ) // End of map that create leave type update promises @@ -590,6 +593,12 @@ function get_and_validate_leave_type(args) { req.param('use_allowance__'+suffix) ); + // If no name for leave type was provided: do nothing - treat case + // as no need to update the leave type + if ( ! name ) { + return false; + } + // VPP TODO move that into resusable component let throw_user_error = function(message){ let error = new Error(message); diff --git a/t/integration/crud_leave_type.js b/t/integration/crud_leave_type.js index bca2f3946..809653ba7 100644 --- a/t/integration/crud_leave_type.js +++ b/t/integration/crud_leave_type.js @@ -9,6 +9,8 @@ var test = require('selenium-webdriver/testing'), check_elements_func = require('../lib/check_elements'), By = require('selenium-webdriver').By, config = require('../lib/config'), + Bluebird = require('bluebird'), + expect = require('chai').expect, application_host = config.get_application_host(), leave_type_edit_form_id='#leave_type_edit_form', leave_type_new_form_id ='#leave_type_new_form'; @@ -186,7 +188,7 @@ describe('CRUD for leave types', function(){ .then(function(){ done() }); }); - it('Add rename newly added leave type to start with "M"', function(done){ + it('And rename newly added leave type to start with "M"', function(done){ submit_form_func({ driver : driver, form_params : [{ @@ -246,6 +248,141 @@ describe('CRUD for leave types', function(){ .then(function(){ done() }); }); + it("Add AAA and ZZZ leave types", function(done){ + driver + .findElement(By.css('#add_new_leave_type_btn')) + .then(el => el.click()) + // This is very important line when working with Bootstrap modals! + .then(() => driver.sleep(1000)) + .then(() => submit_form_func({ + driver : driver, + form_params : [{ + selector : leave_type_new_form_id+' input[name="name__new"]', + value : 'ZZZ', + },{ + selector : leave_type_new_form_id+' input[name="use_allowance__new"]', + value : 'on', + tick : true, + }], + submit_button_selector : leave_type_new_form_id+' button[type="submit"]', + message : /Changes to leave types were saved/, + }) + ) + .then(() => driver.findElement(By.css('#add_new_leave_type_btn'))) + .then(el => el.click()) + .then(() => driver.sleep(1000)) + .then(() => submit_form_func({ + driver : driver, + form_params : [{ + selector : leave_type_new_form_id+' input[name="name__new"]', + value : 'AAA', + },{ + selector : leave_type_new_form_id+' input[name="use_allowance__new"]', + value : 'on', + tick : true, + }], + submit_button_selector : leave_type_new_form_id+' button[type="submit"]', + message : /Changes to leave types were saved/, + }) + ) + .then(() => done()); + }); + + it("Ensure AAA is first and ZZZ is last in the list (general settings page)", function(done){ + check_elements_func({ + driver : driver, + elements_to_check : [{ + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_0"]', + value : 'AAA', + },{ + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_1"]', + value : 'Holiday', + },{ + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_2"]', + value : 'Sick Leave', + },{ + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_3"]', + value : 'ZZZ', + }], + }) + .then(() => done()); + }); + + it("Ensure AAA is a first and ZZZ is a last in a list on book holiday modal", function(done){ + driver + .findElements(By.css('select#leave_type option')) + .then(options => Bluebird.map(options, option => { + let option_info = {}; + return option.getAttribute('value') + .then(val => Bluebird.resolve(option_info.value = val)) + .then(() => option.getAttribute('data-tom')) + .then(txt => Bluebird.resolve(option_info.text = txt)) + .then(() => Bluebird.resolve(option_info)); + })) + .then(option_infos => { + expect(option_infos[0], 'AAA is first').to.include({ value : '0', text : 'AAA'}); + expect(option_infos[3], 'ZZZ is last').to.include({ value : '3', text : 'ZZZ'}); + done(); + }); + }); + + it('Mark ZZZ as one to be default one', function(done){ + driver + .findElement(By.css(leave_type_edit_form_id+' input[data-tom-leave-type-order="name_3"]')) + .then(inp => inp.getAttribute('name')) + .then(name => Bluebird.resolve(name.split('__')[1])) + .then(id => submit_form_func({ + driver : driver, + form_params : [{ + selector : leave_type_edit_form_id+' input[type="radio"][value="'+id+'"]', + tick : true, + value : 'on', + }], + submit_button_selector : leave_type_edit_form_id+' button[type="submit"]', + message : /Changes to leave types were saved/, + }) + ) + .then(() => done()); + }); + + it("Ensure AAA is first and ZZZ is last in the list (general settings page)", function(done){ + check_elements_func({ + driver : driver, + elements_to_check : [{ + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_0"]', + value : 'AAA', + },{ + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_1"]', + value : 'Holiday', + },{ + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_2"]', + value : 'Sick Leave', + },{ + selector : leave_type_edit_form_id+' input[data-tom-leave-type-order="name_3"]', + value : 'ZZZ', + }], + }) + .then(() => done()); + }); + + it("Ensure ZZZ is a first and AAA is a second in a list on book holiday modal", function(done){ + driver + .findElements(By.css('select#leave_type option')) + .then(options => Bluebird.map(options, option => { + let option_info = {}; + return option.getAttribute('value') + .then(val => Bluebird.resolve(option_info.value = val)) + .then(() => option.getAttribute('data-tom')) + .then(txt => Bluebird.resolve(option_info.text = txt)) + .then(() => Bluebird.resolve(option_info)); + })) + .then(option_infos => { + expect(option_infos[0], 'ZZZ is first').to.include({ value : '0', text : 'ZZZ'}); + expect(option_infos[1], 'AAA is last').to.include({ value : '1', text : 'AAA'}); + done(); + }); + }); + after(function(done){ driver.quit().then(function(){ done(); }); }); diff --git a/views/partials/add_new_leave_type_modal.hbs b/views/partials/add_new_leave_type_modal.hbs index 18e34c097..0d6ce3b92 100644 --- a/views/partials/add_new_leave_type_modal.hbs +++ b/views/partials/add_new_leave_type_modal.hbs @@ -10,15 +10,6 @@
    - - {{#each company.leave_types}} - - - - - {{/each}} - - +
    + +
    + +
    +
    +
    From d792619d86df5e2e25c2f93993d95d3501935491 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 9 Jan 2018 15:27:50 +0000 Subject: [PATCH 066/539] Fix issue with last day of the year holiday is not shown. --- lib/model/mixin/user/absence_aware.js | 12 ++++++------ lib/model/mixin/user/company_aware.js | 4 ++-- lib/route/users.js | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/model/mixin/user/absence_aware.js b/lib/model/mixin/user/absence_aware.js index a7f9a0eef..a678751af 100644 --- a/lib/model/mixin/user/absence_aware.js +++ b/lib/model/mixin/user/absence_aware.js @@ -76,7 +76,7 @@ module.exports = function(sequelize){ moment.utc(year).startOf('year').format('YYYY-MM-DD'), moment.utc( year.clone().add((is_multi_year ? 1 : 0), 'years') - ).endOf('year').format('YYYY-MM-DD'), + ).endOf('year').format('YYYY-MM-DD HH:mm'), ] }, date_end : { @@ -84,7 +84,7 @@ module.exports = function(sequelize){ moment.utc( year ).startOf('year').format('YYYY-MM-DD'), moment.utc( year.clone().add((is_multi_year ? 1 : 0), 'years') - ).endOf('year').format('YYYY-MM-DD'), + ).endOf('year').format('YYYY-MM-DD HH:mm'), ] } } @@ -198,13 +198,13 @@ module.exports = function(sequelize){ date_start : { $between : [ moment.utc(year).startOf('year').format('YYYY-MM-DD'), - moment.utc(year).endOf('year').format('YYYY-MM-DD'), + moment.utc(year).endOf('year').format('YYYY-MM-DD HH:mm'), ] }, date_end : { $between : [ moment.utc(year).startOf('year').format('YYYY-MM-DD'), - moment.utc(year).endOf('year').format('YYYY-MM-DD'), + moment.utc(year).endOf('year').format('YYYY-MM-DD HH:mm'), ] } }; @@ -631,13 +631,13 @@ module.exports = function(sequelize){ date_start : { $between : [ moment.utc(year).startOf('year').format('YYYY-MM-DD'), - moment.utc(year).endOf('year').format('YYYY-MM-DD'), + moment.utc(year).endOf('year').format('YYYY-MM-DD HH:mm'), ] }, date_end : { $between : [ moment.utc(year).startOf('year').format('YYYY-MM-DD'), - moment.utc(year).endOf('year').format('YYYY-MM-DD'), + moment.utc(year).endOf('year').format('YYYY-MM-DD HH:mm'), ] } } diff --git a/lib/model/mixin/user/company_aware.js b/lib/model/mixin/user/company_aware.js index e77649d49..aa4cd0eaf 100644 --- a/lib/model/mixin/user/company_aware.js +++ b/lib/model/mixin/user/company_aware.js @@ -49,13 +49,13 @@ module.exports = function(sequelize){ date_start : { $between : [ moment.utc().startOf('year').format('YYYY-MM-DD'), - moment.utc().endOf('year').format('YYYY-MM-DD'), + moment.utc().endOf('year').format('YYYY-MM-DD HH:mm'), ] }, date_end : { $between : [ moment.utc().startOf('year').format('YYYY-MM-DD'), - moment.utc().endOf('year').format('YYYY-MM-DD'), + moment.utc().endOf('year').format('YYYY-MM-DD HH:mm'), ] } } diff --git a/lib/route/users.js b/lib/route/users.js index e028323ac..550b71de0 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -591,13 +591,13 @@ router.get('/', function(req, res) { date_start : { $between : [ moment.utc().startOf('year').format('YYYY-MM-DD'), - moment.utc().endOf('year').format('YYYY-MM-DD'), + moment.utc().endOf('year').format('YYYY-MM-DD HH:mm'), ] }, date_end : { $between : [ moment.utc().startOf('year').format('YYYY-MM-DD'), - moment.utc().endOf('year').format('YYYY-MM-DD'), + moment.utc().endOf('year').format('YYYY-MM-DD HH:mm'), ] } } From 8d0491949a226102210fafc847b691bc7d917f38 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 9 Jan 2018 15:54:05 +0000 Subject: [PATCH 067/539] Add test to ensure the 31 Dec day off is shown on calendar --- .../leave_request/basic_leave_request.js | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/t/integration/leave_request/basic_leave_request.js b/t/integration/leave_request/basic_leave_request.js index c25548c73..9013350d9 100644 --- a/t/integration/leave_request/basic_leave_request.js +++ b/t/integration/leave_request/basic_leave_request.js @@ -317,3 +317,60 @@ describe("Use problematic date with non default date format", function(){ }); }); + +describe("Book the very last day of year to be a holiday", function(){ + + this.timeout( config.get_execution_timeout() ); + + var driver; + + it("Register new company", function(done){ + register_new_user_func({ + application_host : application_host, + }) + .then(function(data){ + driver = data.driver; + done(); + }); + }); + + it("Place new holiday to be the very last day of the year", function(done){ + driver.findElement(By.css('#book_time_off_btn')) + .then(el => el.click()) + // This is very important line when working with Bootstrap modals! + .then(() => driver.sleep(1000)) + .then(() => submit_form_func({ + driver : driver, + form_params : [{ + selector : 'input#from', + value : '2018-12-31', + },{ + selector : 'input#to', + value : '2018-12-31', + }], + message : /New leave request was added/, + }) + ) + .then(() => done()); + }); + + it("Open calendar page and ensure that the very last day of the year is marked as pending", function(done){ + open_page_func({ + url : application_host + 'calendar/?year=2018&show_full_year=1', + driver : driver, + }) + + .then(() => check_booking_func({ + driver : driver, + full_days : [moment('2018-12-31')], + type : 'pended', + })) + + .then(() => done()); + }); + + after(function(done){ + driver.quit().then(function(){ done(); }); + }); + +}); From 6246cb5523c5a7fb952146cff942e1a5e502323c Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 10 Jan 2018 19:40:32 +0000 Subject: [PATCH 068/539] Better navigation on Team view page --- public/js/global.js | 52 +++++++++++++++++++++++++++++++++++++++++++++ views/team_view.hbs | 10 ++++++--- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/public/js/global.js b/public/js/global.js index 23ad262ae..ef2f4e6dd 100644 --- a/public/js/global.js +++ b/public/js/global.js @@ -88,3 +88,55 @@ $('#add_secondary_supervisers_modal').on('show.bs.modal', function (event) { .html('

    Loading...

    ') .load('/settings/departments/available-supervisors/'+department_id+'/'); }); + +/* + * Given URL string return its query paramters as object. + * + * If URL is not provided location of current page is used. + * */ + +function getUrlVars(url){ + if ( ! url ) { + url = window.location.href; + } + var vars = {}, hash; + var hashes = url.slice( url.indexOf('?') + 1).split('&'); + for (var i = 0; i < hashes.length; i++) { + hash = hashes[i].split('='); + vars[hash[0]] = hash[1]; + } + return vars; +} + +/* + * Evend that is fired when user change base date (current month) on Team View page. + * + * */ + +$(document).ready(function(){ + + $('#team_view_month_select_btn') + .datepicker() + .on('changeDate', function(e) { + var url = $(e.currentTarget).data('tom'); + + var form = document.createElement("form"); + form.method = 'GET'; + form.action = url; + + var url_params = getUrlVars( url ); + url_params['date'] = e.format('yyyy-mm'); + + // Move query parameters into the form + $.each( url_params, function(key, val){ + var inp = document.createElement("input"); + inp.name = key; + inp.value = val; + form.appendChild(inp); + }); + + document.body.appendChild(form); + + return form.submit(); + }); +}); diff --git a/views/team_view.hbs b/views/team_view.hbs index 272a064a0..b39b9e894 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -14,12 +14,16 @@ From c237c9c5abcf9468dd2fd5e07db6650aae83c549 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 10 Jan 2018 20:08:56 +0000 Subject: [PATCH 069/539] Remove phantom inputs on Team vew page --- public/js/global.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/js/global.js b/public/js/global.js index ef2f4e6dd..ef237a56e 100644 --- a/public/js/global.js +++ b/public/js/global.js @@ -132,6 +132,7 @@ $(document).ready(function(){ var inp = document.createElement("input"); inp.name = key; inp.value = val; + inp.type = 'hidden'; form.appendChild(inp); }); From efaab2bcfa5e16659fd636b186c4fc417c53090f Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 16 Jan 2018 20:22:31 +0000 Subject: [PATCH 070/539] Rebuild HTML for Team view page --- public/css/style.css | 37 ++++++--- scss/main.scss | 46 +++++++---- t/integration/schedule/company_wide.js | 4 +- t/integration/schedule/user_specific.js | 32 ++++---- t/integration/team_view/deducted_column.js | 6 +- .../team_view/deducted_column__visibility.js | 18 ++-- t/integration/timezone.js | 4 +- t/lib/teamview_check_user.js | 2 +- views/partials/calendar_cell.hbs | 4 +- views/team_view.hbs | 82 +++++++++---------- 10 files changed, 128 insertions(+), 107 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index d666ed174..76a14b29d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -64,13 +64,33 @@ label.label-plain { margin-bottom: 30px; } /* styling calendar*/ +.calendar_month { + width: 100%; } + +.calendar_month td { + text-align: center; + padding-top: 5px; + padding-bottom: 5px; } + +.calendar_month thead { + text-align: center; + font-weight: bolder; } + div.month_container { height: 250px; } -.calendar_month { +.team-view-table { width: 100%; } -.calendar_month td { +.team-view-table td { + padding-top: 5px; + padding-bottom: 5px; } + +td.team-view-header { + height: 43px; + text-align: center; } + +td.calendar_cell { width: 15px !important; min-width: 15px !important; max-width: 15px !important; @@ -79,9 +99,9 @@ div.month_container { padding-top: 5px; padding-bottom: 5px; } -.calendar_month td span { +td.calendar_cell span { position: relative; - left: 10px; + left: 7px; top: -1px; display: block; text-align: center; @@ -109,11 +129,7 @@ td.leave_cell_pended { .modal-dialog { max-width: 450px; } -.team-view-users td { - padding-top: 5px; - padding-bottom: 5px; } - -.team-view-users .left-column-cell { +.left-column-cell { text-overflow: ellipsis; /* Cannot avoid specifying width with pixels... */ width: 165px; @@ -121,9 +137,6 @@ td.leave_cell_pended { overflow: hidden; display: inline-block; } -td.team-view-header { - height: 43px; } - .main-row_header { /* TODO refactore */ border: none; diff --git a/scss/main.scss b/scss/main.scss index 917286924..1cd8487e3 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -91,13 +91,38 @@ label.label-plain { /* styling calendar*/ +.calendar_month { + width : 100%; +} +.calendar_month td { + text-align: center; + padding-top : 5px; + padding-bottom : 5px; +} +.calendar_month thead { + text-align: center; + font-weight: bolder; +} + + div.month_container { height : 250px; } -.calendar_month { - width : 100%; + +.team-view-table { + width : 100%; } -.calendar_month td { +.team-view-table td { + padding-top : 5px; + padding-bottom : 5px; +} +td.team-view-header { + height: 43px; + text-align: center; +} + + +td.calendar_cell { width : 15px !important; min-width : 15px !important; max-width : 15px !important; @@ -106,9 +131,9 @@ div.month_container { padding-top : 5px; padding-bottom : 5px; } -.calendar_month td span { +td.calendar_cell span { position : relative; - left : 10px; + left : 7px; top : -1px; display : block; text-align : center; @@ -136,12 +161,7 @@ td.leave_cell_pended { .modal-dialog { max-width : 450px; } -.team-view-users td { - padding-top : 5px; - padding-bottom : 5px; - -} -.team-view-users .left-column-cell { +.left-column-cell { text-overflow: ellipsis; /* Cannot avoid specifying width with pixels... */ width: 165px; @@ -149,10 +169,6 @@ td.leave_cell_pended { overflow: hidden; display: inline-block; } -td.team-view-header { - height: 43px; -} - .main-row_header { /* TODO refactore */ border: none; diff --git a/t/integration/schedule/company_wide.js b/t/integration/schedule/company_wide.js index d02ae470c..deee83190 100644 --- a/t/integration/schedule/company_wide.js +++ b/t/integration/schedule/company_wide.js @@ -172,7 +172,7 @@ describe("Changing default company wide schedule", function(){ it('... and make sure Wednsday is marked as non-working day', function(done){ driver // We know that 7th of January 2015 is Wednesday - .findElement(By.css('table.calendar_month td.day_7')) + .findElement(By.css('table.team-view-table td.day_7')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -182,7 +182,7 @@ describe("Changing default company wide schedule", function(){ it('... and ensure Monday is still working day', function(done){ driver - .findElement(By.css('table.calendar_month td.day_5')) + .findElement(By.css('table.team-view-table td.day_5')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).not.to.match(/\bweekend_cell\b/); diff --git a/t/integration/schedule/user_specific.js b/t/integration/schedule/user_specific.js index b1f6274d0..3607820bc 100644 --- a/t/integration/schedule/user_specific.js +++ b/t/integration/schedule/user_specific.js @@ -252,7 +252,7 @@ describe('Basic user specific schedule', function(){ it('Make sure that team view shows user A has Sat and Sun as non-working days', function(done){ driver // We know that 7th of January 2015 is Wednesday - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_A+'"] td.day_7')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_A+'"] td.day_7')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.not.match(/\bweekend_cell\b/); @@ -260,7 +260,7 @@ describe('Basic user specific schedule', function(){ }) .then(function(){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_A+'"] td.day_10')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_A+'"] td.day_10')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -269,7 +269,7 @@ describe('Basic user specific schedule', function(){ }) .then(function(){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_A+'"] td.day_11')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_A+'"] td.day_11')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -282,7 +282,7 @@ describe('Basic user specific schedule', function(){ it('Make sure team view shows user B has Wed, Sat, Sun as non-working days', function(done){ driver // We know that 7th of January 2015 is Wednesday - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_B+'"] td.day_7')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_B+'"] td.day_7')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -290,7 +290,7 @@ describe('Basic user specific schedule', function(){ }) .then(function(){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_B+'"] td.day_10')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_B+'"] td.day_10')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -299,7 +299,7 @@ describe('Basic user specific schedule', function(){ }) .then(function(){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_B+'"] td.day_11')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_B+'"] td.day_11')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -586,7 +586,7 @@ describe('Basic user specific schedule', function(){ it('Make sure that team view shows user A has Sat and Sun as non-working days', function(done){ driver // We know that 7th of January 2015 is Wednesday - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_A+'"] td.day_7')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_A+'"] td.day_7')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.not.match(/\bweekend_cell\b/); @@ -594,7 +594,7 @@ describe('Basic user specific schedule', function(){ }) .then(function(){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_A+'"] td.day_10')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_A+'"] td.day_10')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -603,7 +603,7 @@ describe('Basic user specific schedule', function(){ }) .then(function(){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_A+'"] td.day_11')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_A+'"] td.day_11')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -616,7 +616,7 @@ describe('Basic user specific schedule', function(){ it('Make sure team view shows user B also has Sat, Sun as non-working days', function(done){ driver // We know that 7th of January 2015 is Wednesday - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_B+'"] td.day_7')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_B+'"] td.day_7')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.not.match(/\bweekend_cell\b/); @@ -624,7 +624,7 @@ describe('Basic user specific schedule', function(){ }) .then(function(){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_B+'"] td.day_10')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_B+'"] td.day_10')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -633,7 +633,7 @@ describe('Basic user specific schedule', function(){ }) .then(function(){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_B+'"] td.day_11')) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_B+'"] td.day_11')) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -838,7 +838,7 @@ describe('Populate company wide schedule before using user specific one', functi it('Ensure team view shows user A has Mon, Tue, Wed as working days', function(done){ Promise.map([5,6,7], function(day_number){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_A+'"] td.day_'+day_number)) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_A+'"] td.day_'+day_number)) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.not.match(/\bweekend_cell\b/); @@ -851,7 +851,7 @@ describe('Populate company wide schedule before using user specific one', functi it('Ensure team view shows user A has Thu, Fri, Sat, Sun as non-working days', function(done){ Promise.map([8,9,10,11], function(day_number){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_A+'"] td.day_'+day_number)) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_A+'"] td.day_'+day_number)) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); @@ -864,7 +864,7 @@ describe('Populate company wide schedule before using user specific one', functi it('Ensure team view shows user B has Mon, Tue, Wed, Thu as working days', function(done){ Promise.map([5,6,7,8], function(day_number){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_B+'"] td.day_'+day_number)) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_B+'"] td.day_'+day_number)) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.not.match(/\bweekend_cell\b/); @@ -877,7 +877,7 @@ describe('Populate company wide schedule before using user specific one', functi it('Ensure team view shows user B has Fri, Sat, Sun as non-working days', function(done){ Promise.map([9,10,11], function(day_number){ return driver - .findElement(By.css('table.calendar_month tr[data-vpp="'+user_id_B+'"] td.day_'+day_number)) + .findElement(By.css('table.team-view-table tr[data-vpp-user-list-row="'+user_id_B+'"] td.day_'+day_number)) .then(function(el){ return el.getAttribute('class'); }) .then(function(css){ expect(css).to.match(/\bweekend_cell\b/); diff --git a/t/integration/team_view/deducted_column.js b/t/integration/team_view/deducted_column.js index 3d7f409ff..63ae8cc53 100644 --- a/t/integration/team_view/deducted_column.js +++ b/t/integration/team_view/deducted_column.js @@ -338,7 +338,7 @@ describe('Case when holidays spans through more then one month and is devided by url : application_host + 'calendar/teamview/?date=2016-08', driver : driver, }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt, 'Ensure that system shows 9 days as deducted') @@ -352,7 +352,7 @@ describe('Case when holidays spans through more then one month and is devided by url : application_host + 'calendar/teamview/?date=2016-07', driver : driver, }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt, 'Ensure that system shows 1.5 days as deducted') @@ -366,7 +366,7 @@ describe('Case when holidays spans through more then one month and is devided by url : application_host + 'calendar/teamview/?date=2016-09', driver : driver, }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt, 'Ensure that system shows 2 days as deducted') diff --git a/t/integration/team_view/deducted_column__visibility.js b/t/integration/team_view/deducted_column__visibility.js index f1d26f452..254319de2 100644 --- a/t/integration/team_view/deducted_column__visibility.js +++ b/t/integration/team_view/deducted_column__visibility.js @@ -220,21 +220,21 @@ describe('Check that values for new columns are shown only for employess current driver : driver, }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql('0'); return Promise.resolve(1); }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql('0'); return Promise.resolve(1); }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql('0'); @@ -267,21 +267,21 @@ describe('Check that values for new columns are shown only for employess current driver : driver, }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql(''); return Promise.resolve(1); }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql('0'); return Promise.resolve(1); }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql('0'); @@ -314,21 +314,21 @@ describe('Check that values for new columns are shown only for employess current driver : driver, }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_A}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql(''); return Promise.resolve(1); }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_B}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql(''); return Promise.resolve(1); }) - .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] td.teamview-deducted-days`))) + .then(() => driver.findElement(By.css(`tr[data-vpp-user-list-row="${user_id_C}"] span.teamview-deducted-days`))) .then(el => el.getText()) .then(txt => { expect(txt).to.be.eql('0'); diff --git a/t/integration/timezone.js b/t/integration/timezone.js index cb5958dda..2b4bd9b2d 100644 --- a/t/integration/timezone.js +++ b/t/integration/timezone.js @@ -111,7 +111,7 @@ describe('Check Time zones', function(){ driver : driver, }) .then(() => driver.findElement(By.css( - 'table.calendar_month td.half_1st.day_'+moment(today_aus).format('D')+'.current_day_cell' + 'table.team-view-table td.half_1st.day_'+moment(today_aus).format('D')+'.current_day_cell' ))) .then(el => { expect(el, 'Ensure that current date is marked correctly').to.exist; @@ -224,7 +224,7 @@ describe('Check Time zones', function(){ driver : driver, }) .then(() => driver.findElement(By.css( - 'table.calendar_month td.half_1st.day_'+moment(today_usa).format('D')+'.current_day_cell' + 'table.team-view-table td.half_1st.day_'+moment(today_usa).format('D')+'.current_day_cell' ))) .then(el => { expect(el, 'Ensure that current date is marked correctly').to.exist; diff --git a/t/lib/teamview_check_user.js b/t/lib/teamview_check_user.js index daa73ff31..3fc478d72 100644 --- a/t/lib/teamview_check_user.js +++ b/t/lib/teamview_check_user.js @@ -36,7 +36,7 @@ module.exports = bluebird.promisify( function(args, callback){ .then(function(data){ return data.driver - .findElements(By.css( 'tr.teamview-user-list-row > td > ' + (is_link ? 'a' : 'span') )) + .findElements(By.css( 'tr.teamview-user-list-row > td.cross-link > ' + (is_link ? 'a' : 'span') )) .then(function(elements){ expect(elements.length).to.be.equal( emails.length ); return bluebird.resolve(data); diff --git a/views/partials/calendar_cell.hbs b/views/partials/calendar_cell.hbs index 0c66ebd6b..37163f397 100644 --- a/views/partials/calendar_cell.hbs +++ b/views/partials/calendar_cell.hbs @@ -1,5 +1,5 @@ -
    {{day.val}}  
    -
    - - {{# each users_info}} - {{# with this.user_row}} - - - - - - + + + + + + - {{/with}} {{/each}}
    +
    + - - - - {{# each users_and_leaves}} - - - + - - {{/each}} -
    {{# if ../logged_user.admin}} {{#with this.user }}{{ this.full_name }}{{/with}} {{else}}{{#with this.user }}{{ this.full_name }}{{/with}}{{/if}} - {{# if statistics }}{{ statistics.deducted_days }}{{/if}} +
    +
    -
    -
    - - {{# each users_and_leaves.0.days }} {{/each}} - {{# each users_and_leaves}} - - {{# each this.days}} - {{> calendar_cell day = this}} - {{/each}} - - {{/each}} + + {{# each users_and_leaves}} + + + + {{# each this.days}} + {{> calendar_cell day = this}} + {{/each}} + + {{/each}}
    {{as_date_formatted this.moment 'dd'}}
    + + {{# if statistics }}{{ statistics.deducted_days }}{{/if}} + +
    -
     
    From d16325c6c78f353e61c2c5c28c8b55629d1d1bb2 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 11 Jan 2018 06:59:33 +0000 Subject: [PATCH 071/539] Add link to import page from users page --- views/users.hbs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/views/users.hbs b/views/users.hbs index 717f8d6c6..08d36497a 100644 --- a/views/users.hbs +++ b/views/users.hbs @@ -8,7 +8,16 @@
    {{company.name}}'s staff
    - Add new employee +
    + Add new employee + + +
    From b550237215cef16230005b77e5e4430e491bc926 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 11 Jan 2018 07:03:33 +0000 Subject: [PATCH 072/539] Add link to import page from add new employee page --- views/user_add.hbs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/views/user_add.hbs b/views/user_add.hbs index 6782475fc..7d69b244f 100644 --- a/views/user_add.hbs +++ b/views/user_add.hbs @@ -3,10 +3,17 @@

    New employee

    -

    Adding new employee account

    - {{> show_flash_messages }} +
    +
    Adding new employee account
    + +
    + +
     
    +
    From 5d935132235b161fdc6d809e08e06c9d6f968ba1 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 12 Jan 2018 06:51:58 +0000 Subject: [PATCH 073/539] First implementation of import users from .csv --- lib/model/db/company.js | 10 +++ lib/model/db/user.js | 5 +- lib/model/mixin/user/company_aware.js | 15 ---- lib/route/users.js | 113 ++++++++++++++++++++++++-- package.json | 1 + views/users_import.hbs | 60 ++++++++++++++ 6 files changed, 179 insertions(+), 25 deletions(-) create mode 100644 views/users_import.hbs diff --git a/lib/model/db/company.js b/lib/model/db/company.js index c0eb1482e..5ee127975 100644 --- a/lib/model/db/company.js +++ b/lib/model/db/company.js @@ -113,6 +113,16 @@ module.exports = function(sequelize, DataTypes) { }, loadScope : function( models ) { + + Company.addScope( + 'with_all_users', + { + include : [ + { model : models.User, as : 'users' }, + ] + } + ); + Company.addScope( 'with_active_users', // The scope needs to be dynamic as the criteria for active users is based diff --git a/lib/model/db/user.js b/lib/model/db/user.js index 0f80e44e0..5e650292f 100644 --- a/lib/model/db/user.js +++ b/lib/model/db/user.js @@ -140,7 +140,10 @@ function get_instance_methods(sequelize) { // Check if current user is admin, then fetch all users form company if ( this_user.is_admin() ) { - return this_user.get_company_with_all_users() + return this_user + .getCompany({ + scope : ['with_all_users'], + }) .then(function(company){ return Promise.resolve( company.users ); }); diff --git a/lib/model/mixin/user/company_aware.js b/lib/model/mixin/user/company_aware.js index aa4cd0eaf..40d536d77 100644 --- a/lib/model/mixin/user/company_aware.js +++ b/lib/model/mixin/user/company_aware.js @@ -130,21 +130,6 @@ module.exports = function(sequelize){ }; - this.get_company_with_all_users = function(){ - return this.getCompany({ - include : [ - { - model : sequelize.models.User, - as : 'users', - }, - ], - order : [ - [{ model : sequelize.models.User, as : 'users' }, 'lastname'] - ] - }); - }; - - this.get_company_with_all_leave_types = function() { return this.getCompany({ include : [{ diff --git a/lib/route/users.js b/lib/route/users.js index 550b71de0..75752e772 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -1,15 +1,19 @@ "use strict"; -var express = require('express'), - router = express.Router(), - validator = require('validator'), - Promise = require('bluebird'), - moment = require('moment'), - _ = require('underscore'), - uuid = require('node-uuid'), - LeaveCollectionUtil = require('../model/leave_collection')(), - EmailTransport = require('../email'); +const + express = require('express'), + router = express.Router(), + validator = require('validator'), + Promise = require('bluebird'), + moment = require('moment'), + _ = require('underscore'), + uuid = require('node-uuid'), + csv = Promise.promisifyAll(require('csv')), + fs = require("fs"), + formidable= require('formidable'), + LeaveCollectionUtil = require('../model/leave_collection')(), + EmailTransport = require('../email'); // Make sure that current user is authorized to deal with settings router.all(/.*/, require('../middleware/ensure_user_is_admin')); @@ -109,6 +113,97 @@ router.post('/add/', function(req, res){ }); }); +router.get('/import/', function(req, res){ + req.user + .getCompany() + .then(company => res.render( + 'users_import', { + company : company, + } + )); +}); + +router.post('/import/', function(req, res){ + let + form = new formidable.IncomingForm(), + model = req.app.get('db_model'), + parseAsync = Promise.promisify(form.parse); + + parseAsync + .call(form, req) + .then(args => { + + let files = args[1]; + + if (files.users_import.size === 0) { + throw new Error('No .CSV file to restore from was provided'); + } + + return fs.readFileAsync(files.users_import.path, "utf8"); + }) + .then(csv_data_string => csv.parseAsync(csv_data_string,{trim:true})) + .then(parsed_data => { + + // TODO consider limiting number of users + + // Ditch header + parsed_data.shift() + + return Promise + .map(parsed_data, user_vector => { + let attributes = { + email : user_vector[0], + lastname : user_vector[1], + name : user_vector[2], + password : uuid.v4(), + companyId : req.user.companyId, + DepartmentId : XXXXXXX, + }; + + return model.User.create(attributes) + }) + .then(new_users => { + // NOTE consider sending emails here + return Promise.resolve(new_users); + }); + }) + .then(function(company){ + req.session.flash_message('Successfully imported users'); + res.redirect_with_session('/users/import/'); + }) + .catch(function(error){ + req.session.flash_error('Failed to import users, reason: '+error); + res.redirect_with_session('/users/import/'); + }); +}); + +router.post('/import-sample/', function(req, res){ + + req.user + .getCompany({ + scope : ['with_active_users', 'with_simple_departments'], + }) + .then(company => { + + res.attachment( + company.name_for_machine()+'.csv' + ); + + let content = company.users.map( user => [ + user.email, + user.name, + user.lastname, + company.departments.find( dep => dep.id === user.DepartmentId ).name + ]); + + content.unshift( ['email','lastname', 'name', 'department'] ); + + return csv.stringifyAsync( content ); + }) + .then(csv_data_string => res.send(csv_data_string)); + +}); + router.get('/edit/:user_id/', function(req, res){ var user_id = validator.trim(req.param('user_id')); diff --git a/package.json b/package.json index f38695cf6..4d7345c06 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "body-parser": "^1.8.4", "connect-session-sequelize": "3.0.0", "cookie-parser": "^1.3.5", + "csv": "^0.4.6", "debug": "~2.0.0", "express": "^4.13.4", "express-handlebars": "^3.0.0", diff --git a/views/users_import.hbs b/views/users_import.hbs new file mode 100644 index 000000000..a6010e8d9 --- /dev/null +++ b/views/users_import.hbs @@ -0,0 +1,60 @@ + +{{> header }} + +

    Employees bulk import

    + +{{> show_flash_messages }} + +
    +
    Quick employees import
    + +
    + +
     
    + +
    + +
    + +

    Download sample

    + + +
    + +
    + +
    +
    +
    +
    + Download sample prepopulated with current employees. +
    +
    + + + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    + + +
    + +
    + +
     
    + +{{> footer }} From 176a575608cddcecbb48b93a7c1895c6a5bf5eda Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 19 Jan 2018 07:26:02 +0000 Subject: [PATCH 074/539] Re-organise importing code so controller is lighter. --- lib/error/index.js | 80 +++++++++++++++++++++ lib/model/user_importer.js | 138 +++++++++++++++++++++++++++++++++++++ lib/route/users.js | 83 ++++++++++++++-------- 3 files changed, 273 insertions(+), 28 deletions(-) create mode 100644 lib/error/index.js create mode 100644 lib/model/user_importer.js diff --git a/lib/error/index.js b/lib/error/index.js new file mode 100644 index 000000000..b985d7135 --- /dev/null +++ b/lib/error/index.js @@ -0,0 +1,80 @@ + +"use strict"; + +const + Joi = require('Joi'); + +const schema_throw_user_error = [ + Joi.object() + .keys({ + system_error : Joi.string().required(), + user_error : Joi.string().required(), + }), + Joi.string() +]; + +function throw_user_error(args) { + const validate_args = Joi.validate(args, schema_throw_user_error); + + // Ensure all necesasry info was privided + if ( validate_args.error) { + // Tricky case here as we failed to rais exception, so log all data we have + // and still rais generic exception + console.log( + 'throw_user_error got invalid parameters: ' + + validate_args.annotate() + ); + console.dir(args); + throw new Error('Failed to throw user errors'); + } + + let system_error_message, + user_error_message; + + // Special case when user is lazy and specified generic error message to be + // used for system and customer level + if ( typeof args === 'string' ) { + system_error_message = user_error_message = args; + } else { + system_error_message = args.system_error; + user_error_message = args.user_error; + } + + let exception = new Error( system_error_message ); + + if ( user_error_message ) { + exception.user_error_message = user_error_message; + } + + throw exception; +} + +function extract_system_error_message(error) { + if ( ! error ) { + return null; + } + + return error; +} + +function extract_user_error_message(error) { + if ( ! error ) { + return null; + } + + if (typeof error === 'string') { + return error; + } + + if ( error.hasOwnProperty( 'user_error_message' )) { + return error.user_error_message; + } + + return 'N/A'; +} + +module.exports = { + throw_user_error : throw_user_error, + extract_user_error_message : extract_user_error_message, + extract_system_error_message : extract_system_error_message, +}; diff --git a/lib/model/user_importer.js b/lib/model/user_importer.js new file mode 100644 index 000000000..8f8608266 --- /dev/null +++ b/lib/model/user_importer.js @@ -0,0 +1,138 @@ + +"use strict"; + +const + Joi = require('joi'), + uuid = require('node-uuid'), + Promise = require('bluebird'), + _ = require('underscore'), + Exception = require('../error'), + Models = require('./db'); + +const add_user_interface_schema = Joi.object().keys({ + email : Joi.string().email(), + lastname : Joi.string(), + name : Joi.string(), + company_id : Joi.number().integer().positive(), + department_id : Joi.number().integer().positive(), + }); + + +function add_user(args) { + let validate_args = Joi.validate(args, add_user_interface_schema); + + if (validate_args.error) { + console.log('An error occured when validatin parameters for add_user: '); + console.dir(validate_args); + Exception.throw_user_error({ + system_error : 'Failed to add new due to validation errors', + user_error : 'Failed to add user', + }); + } + + return Models.User.create({ + email : args.email, + lastname : args.lastname, + name : args.name, + password : uuid.v4(), + companyId : args.company_id, + DepartmentId : args.department_id, + }) + .then(new_users => { + // NOTE consider sending emails here + return Promise.resolve(new_users); + }); +} + +function add_users_in_bulk(args) { + let bulk_header = args.bulk_header, + bulk_data = args.bulk_data, + company_id = args.to_company_id; + + let company, + email_vector_index = 0, + lastname_vector_index = 1, + name_vector_index = 2, + department_vector_index = 3; + + return Models.Company.scope('with_simple_departments').findOne({ + where : { id : company_id }, + }) + + + // Validate department names and replace names with IDs + .then(cmp => { + company = cmp; + let dep_name_to_id = _.object( + company.departments.map(dep => [dep.name, dep.id]) + ); + + let with_invalid_departments = _.filter( + bulk_data, vector => ! dep_name_to_id[ vector[ department_vector_index] ] + ); + + if (with_invalid_departments.length > 0) { + let unknown_departments = with_invalid_departments + .map(vector => '"'+vector[department_vector_index]+'"') + .join(', '); + + Error.throw_user_error({ + user_error : 'Following departments could not be found in ' + + company.name + ' account: ' + unknown_departments, + system_error : 'While importing users to company ' + + company.id + ' there were unknown departments ' + + unknown_departments, + }); + } + + bulk_data.forEach( + vector => vector[ department_vector_index ] = dep_name_to_id[ vector[ department_vector_index ] ] + ); + + return Promise.resolve(); + }) + + // Add users + .then(() => { + return Promise.map(bulk_data, vector => { + let email = vector[ email_vector_index ]; + + return add_user({ + email : email, + lastname : vector[ lastname_vector_index ], + name : vector[ name_vector_index ], + department_id : vector[ department_vector_index ], + company_id : company_id, + }) + .catch(error => { + return Promise.resolve({ + error : error, + email : email, + }); + }); + }, { + concurrency : 2, + }) + }) + + // Sort out successfully creted users and errors + .then(users_or_errors => { + let result = { + users : [], + errors : [], + }; + + users_or_errors.forEach( item => { + item.hasOwnProperty('error') + ? result.errors.push( item ) + : result.users.push( item ) + }); + + return Promise.resolve(result); + }); + +} + +module.exports = { + add_users_in_bulk : add_users_in_bulk, +}; diff --git a/lib/route/users.js b/lib/route/users.js index 75752e772..127b88630 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -11,9 +11,11 @@ const uuid = require('node-uuid'), csv = Promise.promisifyAll(require('csv')), fs = require("fs"), - formidable= require('formidable'), + formidable = require('formidable'), LeaveCollectionUtil = require('../model/leave_collection')(), - EmailTransport = require('../email'); + Exception = require('../error'), + UserImporter = require('../model/user_importer'), + EmailTransport = require('../email'); // Make sure that current user is authorized to deal with settings router.all(/.*/, require('../middleware/ensure_user_is_admin')); @@ -100,7 +102,7 @@ router.post('/add/', function(req, res){ }) .catch(function(error){ - console.error( + console.log( 'An error occurred when trying to add new user account by user '+req.user.id + ' : ' + error ); @@ -136,7 +138,17 @@ router.post('/import/', function(req, res){ let files = args[1]; if (files.users_import.size === 0) { - throw new Error('No .CSV file to restore from was provided'); + Exception.throw_user_error({ + user_error : 'No .CSV file to restore from was provided', + system_error : 'User ' + req.user.id + ' tried to import employees ' + + 'without submitting .CSV file', + }); + } else if ( files.users_import.size > 2097152 ) { + Exception.throw_user_error({ + user_error : '.CSV file could not be bigger then 2M', + system_error : 'User ' + req.user.id + ' tried to submit file bigger then ' + + '2M', + }); } return fs.readFileAsync(files.users_import.path, "utf8"); @@ -144,35 +156,50 @@ router.post('/import/', function(req, res){ .then(csv_data_string => csv.parseAsync(csv_data_string,{trim:true})) .then(parsed_data => { - // TODO consider limiting number of users - - // Ditch header - parsed_data.shift() - - return Promise - .map(parsed_data, user_vector => { - let attributes = { - email : user_vector[0], - lastname : user_vector[1], - name : user_vector[2], - password : uuid.v4(), - companyId : req.user.companyId, - DepartmentId : XXXXXXX, - }; - - return model.User.create(attributes) - }) - .then(new_users => { - // NOTE consider sending emails here - return Promise.resolve(new_users); + // Limit number of employees to be imported at one go + // + if (parsed_data.length > 201) { + Exception.throw_user_error({ + user_error : 'Cannot import more then 200 employees per one go. ' + + 'Please splite .CSV file into chunks of no more then 200 employees ' + + 'and process them each at the time', + system_error : 'User ' + req.user.id + ' tried to import more then 200 ' + + 'user at one time' }); + } + + return UserImporter.add_users_in_bulk({ + to_company_id : req.user.companyId, + bulk_header : parsed_data.shift(), + bulk_data : parsed_data, + }); }) - .then(function(company){ - req.session.flash_message('Successfully imported users'); + .then(action_result => { + console.dir(action_result); + if ( action_result.users.length > 0 ) { + req.session.flash_message( + 'Successfully imported users: ' + + action_result.users.map(user => user.email).join(', ') + ); + } + if (action_result.errors.length > 0) { + req.session.flash_message( + 'Failed to import those users users: ' + + action_result.errors.map(err => err.email +': '+err.error).join('; ') + ); + } res.redirect_with_session('/users/import/'); }) .catch(function(error){ - req.session.flash_error('Failed to import users, reason: '+error); + console.error( + '>>>>>>> An error occurred when trying to import users for company' + + req.user.companyId + + '. Reason: ' + Exception.extract_system_error_message(error) + ); + req.session.flash_error( + 'Failed to import users, reason: ' + + Exception.extract_user_error_message(error) + ); res.redirect_with_session('/users/import/'); }); }); From 8e0b4eac27bf968b1ac492758131ddc34f73a190 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 19 Jan 2018 13:15:03 +0000 Subject: [PATCH 075/539] Validate email for new user to be free in single place --- lib/error/index.js | 2 ++ lib/model/user_importer.js | 68 +++++++++++++++++++++++++++++--------- lib/route/users.js | 47 +++++++++++++------------- 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/lib/error/index.js b/lib/error/index.js index b985d7135..2eeaca536 100644 --- a/lib/error/index.js +++ b/lib/error/index.js @@ -46,6 +46,8 @@ function throw_user_error(args) { exception.user_error_message = user_error_message; } + exception.tom_error = true; + throw exception; } diff --git a/lib/model/user_importer.js b/lib/model/user_importer.js index 8f8608266..ad7ba3632 100644 --- a/lib/model/user_importer.js +++ b/lib/model/user_importer.js @@ -9,7 +9,7 @@ const Exception = require('../error'), Models = require('./db'); -const add_user_interface_schema = Joi.object().keys({ +const add_user_interface_schema = Joi.object().required().keys({ email : Joi.string().email(), lastname : Joi.string(), name : Joi.string(), @@ -30,14 +30,19 @@ function add_user(args) { }); } - return Models.User.create({ + return Promise.resolve() + // Ensure provided email is free to use + .then(() => validate_email_to_be_free({ email : args.email })) + + // Create new user record + .then(() => Models.User.create({ email : args.email, lastname : args.lastname, name : args.name, password : uuid.v4(), companyId : args.company_id, DepartmentId : args.department_id, - }) + })) .then(new_users => { // NOTE consider sending emails here return Promise.resolve(new_users); @@ -97,19 +102,20 @@ function add_users_in_bulk(args) { return Promise.map(bulk_data, vector => { let email = vector[ email_vector_index ]; - return add_user({ - email : email, - lastname : vector[ lastname_vector_index ], - name : vector[ name_vector_index ], - department_id : vector[ department_vector_index ], - company_id : company_id, - }) - .catch(error => { - return Promise.resolve({ - error : error, - email : email, + return Promise.resolve() + .then(() => add_user({ + email : email, + lastname : vector[ lastname_vector_index ], + name : vector[ name_vector_index ], + department_id : vector[ department_vector_index ], + company_id : company_id, + })) + .catch(error => { + return Promise.resolve({ + error : error, + email : email, + }); }); - }); }, { concurrency : 2, }) @@ -133,6 +139,36 @@ function add_users_in_bulk(args) { } +const validate_email_to_be_free_schema = Joi.object().required().keys({ + email : Joi.string().email().required(), + }); + +function validate_email_to_be_free(args) { + let validate_args = Joi.validate(args, validate_email_to_be_free_schema); + + if (validate_args.error) { + Exception.throw_user_error({ + system_error : 'validate_email_to_be_free failed arguments validation', + user_error : 'Failed to validate email', + }); + } + + return Models + .User + .find_by_email(args.email) + .then(user => { + + if (user) { + Exception.throw_user_error( + 'Email is already in use' + ); + } + + return Promise.resolve(); + }); +} + module.exports = { - add_users_in_bulk : add_users_in_bulk, + add_users_in_bulk : add_users_in_bulk, + validate_email_to_be_free : validate_email_to_be_free, }; diff --git a/lib/route/users.js b/lib/route/users.js index 127b88630..162f223ca 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -32,8 +32,10 @@ router.get('/add/', function(req, res){ router.post('/add/', function(req, res){ - var current_company, - model = req.app.get('db_model'); + const model = req.app.get('db_model'); + + let current_company, + new_user_attributes; req.user .get_company_for_add_user() @@ -41,7 +43,7 @@ router.post('/add/', function(req, res){ current_company = company; - var new_user_attributes = get_and_validate_user_parameters({ + new_user_attributes = get_and_validate_user_parameters({ req : req, item_name : 'user', departments : company.departments, @@ -61,20 +63,11 @@ router.post('/add/', function(req, res){ }) // Make sure that we do not add user with existing emails - .then(function(new_user_attributes){ - return model.User.find_by_email(new_user_attributes.email) - .then(function(user){ - - if (user) { - req.session.flash_error('Email is already in use'); - throw new Error('Email is already used'); - } - - return Promise.resolve(new_user_attributes); - }); - }) + .then(() => UserImporter + .validate_email_to_be_free({ email : new_user_attributes.email }) + ) - .then(new_user_attributes => { + .then(() => { // User table is not going to hold adjustments delete new_user_attributes.adjustment; @@ -102,16 +95,22 @@ router.post('/add/', function(req, res){ }) .catch(function(error){ - console.log( - 'An error occurred when trying to add new user account by user '+req.user.id - + ' : ' + error - ); + console.log( + 'An error occurred when trying to add new user account by user '+req.user.id + + ' : ' + error + ); - req.session.flash_error( - 'Failed to add new user' - ); + console.dir(error); + + if ( error && error.tom_error) { + req.session.flash_error( Exception.extract_user_error_message(error) ); + } + + req.session.flash_error( + 'Failed to add new user' + ); - return res.redirect_with_session('../add/'); + return res.redirect_with_session('../add/'); }); }); From d72be218e480c2764a8c778006278a7da664ed54 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 19 Jan 2018 15:36:55 +0000 Subject: [PATCH 076/539] Use single code for adding new users --- lib/model/user_importer.js | 58 +++++++++++++++++++++++++++------- lib/route/users.js | 64 +++++++++++++++++--------------------- 2 files changed, 75 insertions(+), 47 deletions(-) diff --git a/lib/model/user_importer.js b/lib/model/user_importer.js index ad7ba3632..0f5c187ee 100644 --- a/lib/model/user_importer.js +++ b/lib/model/user_importer.js @@ -15,6 +15,12 @@ const add_user_interface_schema = Joi.object().required().keys({ name : Joi.string(), company_id : Joi.number().integer().positive(), department_id : Joi.number().integer().positive(), + + start_date : Joi.string().optional(), + end_date : Joi.string().default(null).allow(null), + admin : Joi.boolean().default(false), + auto_approve : Joi.boolean().default(false), + password : Joi.string().default(() => uuid.v4, 'Populate defaul password'), }); @@ -30,23 +36,49 @@ function add_user(args) { }); } + let attributes = {}; + + attributes.email = args.email; + attributes.lastname = args.lastname; + attributes.name = args.name; + attributes.companyId = args.company_id; + attributes.DepartmentId = args.department_id; + + attributes.password = Models.User.hashify_password(args.password); + attributes.admin = args.admin; + attributes.auto_approve = args.auto_approve; + attributes.end_date = args.end_date; + + // Pass start date inky if it is set, otherwise rely on database to use + // default value + if (args.start_date) { + attributes.start_date = args.start_date; + } + return Promise.resolve() + + // Ensure given department ID is owned by given company ID + .then(() => Models.Department + .findOne({ + where : { id : args.department_id, companyId : args.company_id }, + }) + .then( department => { + if ( ! department ) { + Exception.throw_user_error({ + system_error : 'Mismatch in department/company IDs when creating new user ' + + args.department_id + '/' + args.company_id, + user_error : 'Used wrong department', + }); + } + return Promise.resolve(); + }) + ) + // Ensure provided email is free to use .then(() => validate_email_to_be_free({ email : args.email })) // Create new user record - .then(() => Models.User.create({ - email : args.email, - lastname : args.lastname, - name : args.name, - password : uuid.v4(), - companyId : args.company_id, - DepartmentId : args.department_id, - })) - .then(new_users => { - // NOTE consider sending emails here - return Promise.resolve(new_users); - }); + .then(() => Models.User.create(attributes)); } function add_users_in_bulk(args) { @@ -169,6 +201,8 @@ function validate_email_to_be_free(args) { } module.exports = { + add_user : add_user, add_users_in_bulk : add_users_in_bulk, validate_email_to_be_free : validate_email_to_be_free, }; + diff --git a/lib/route/users.js b/lib/route/users.js index 162f223ca..6e5c9982c 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -32,7 +32,9 @@ router.get('/add/', function(req, res){ router.post('/add/', function(req, res){ - const model = req.app.get('db_model'); + const + model = req.app.get('db_model'), + Email = new EmailTransport(); let current_company, new_user_attributes; @@ -51,15 +53,7 @@ router.post('/add/', function(req, res){ require_password : (company.ldap_auth_enabled ? false : true ), }); - // TODO: make department responsible for adding new useR? - new_user_attributes.password = model.User.hashify_password( - // ... Instead use random string as password - // (users will be able to reset it when needed) - new_user_attributes.password || uuid.v4() - ); - new_user_attributes.companyId = company.id; - - return Promise.resolve(new_user_attributes); + return Promise.resolve(); }) // Make sure that we do not add user with existing emails @@ -67,39 +61,39 @@ router.post('/add/', function(req, res){ .validate_email_to_be_free({ email : new_user_attributes.email }) ) - .then(() => { - - // User table is not going to hold adjustments - delete new_user_attributes.adjustment; - - return model.User.create(new_user_attributes) - }) - - .then(function(new_user){ - var Email = new EmailTransport(); - - return Email.promise_add_new_user_email({ - company : current_company, - admin_user : req.user, - new_user : new_user, - }); - }) + // Add new user to database + .then(() => UserImporter.add_user({ + name : new_user_attributes.name, + lastname : new_user_attributes.lastname, + email : new_user_attributes.email, + department_id : new_user_attributes.DepartmentId, + start_date : new_user_attributes.start_date, + end_date : new_user_attributes.end_date, + admin : new_user_attributes.admin, + auto_approve : new_user_attributes.auto_approve, + company_id : req.user.companyId, + password : new_user_attributes.password, + })) + + .then(new_user => Email.promise_add_new_user_email({ + company : current_company, + admin_user : req.user, + new_user : new_user, + })) .then(function(){ - if ( req.session.flash_has_errors() ) { - return res.redirect_with_session('../add/'); - } else { - req.session.flash_message('New user account successfully added'); - return res.redirect_with_session('../'); - } + if ( req.session.flash_has_errors() ) { + return res.redirect_with_session('../add/'); + } else { + req.session.flash_message('New user account successfully added'); + return res.redirect_with_session('../'); + } }) .catch(function(error){ console.log( 'An error occurred when trying to add new user account by user '+req.user.id - + ' : ' + error ); - console.dir(error); if ( error && error.tom_error) { From 9b080cae742c3fd94c2413841308ddbc03031e4c Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 19 Jan 2018 15:55:17 +0000 Subject: [PATCH 077/539] Amend error messages for users --- lib/route/users.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/route/users.js b/lib/route/users.js index 6e5c9982c..ba569ee4d 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -171,15 +171,14 @@ router.post('/import/', function(req, res){ console.dir(action_result); if ( action_result.users.length > 0 ) { req.session.flash_message( - 'Successfully imported users: ' + 'Successfully imported users with following emails: ' + action_result.users.map(user => user.email).join(', ') ); } if (action_result.errors.length > 0) { - req.session.flash_message( - 'Failed to import those users users: ' - + action_result.errors.map(err => err.email +': '+err.error).join('; ') - ); + action_result.errors.forEach(err => req.session.flash_error( + 'Failed to add user ' + err.email + '. Reason: ' + err.error + )); } res.redirect_with_session('/users/import/'); }) From fcd4ae072786f1c39e7124936130be9c146c83ef Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 07:16:03 +0000 Subject: [PATCH 078/539] Simple test for user import --- lib/route/users.js | 4 +- t/integration/users_import.js | 133 ++++++++++++++++++++++++++++++++++ t/lib/add_new_user.js | 2 +- t/lib/submit_form.js | 3 + views/user_add.hbs | 2 +- views/users_import.hbs | 2 +- 6 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 t/integration/users_import.js diff --git a/lib/route/users.js b/lib/route/users.js index ba569ee4d..9f530d526 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -172,7 +172,7 @@ router.post('/import/', function(req, res){ if ( action_result.users.length > 0 ) { req.session.flash_message( 'Successfully imported users with following emails: ' - + action_result.users.map(user => user.email).join(', ') + + action_result.users.map(user => user.email).sort().join(', ') ); } if (action_result.errors.length > 0) { @@ -832,7 +832,9 @@ function get_and_validate_user_parameters(args) { // Validate provided parameters + console.log('>>>>> ' + email); if (!validator.isEmail(email)) { + console.log('>>>>>: ' + email); req.session.flash_error( 'New email of '+item_name+' should be valid email address' ); diff --git a/t/integration/users_import.js b/t/integration/users_import.js new file mode 100644 index 000000000..ea3a82591 --- /dev/null +++ b/t/integration/users_import.js @@ -0,0 +1,133 @@ + +'use strict'; + +var test = require('selenium-webdriver/testing'), + By = require('selenium-webdriver').By, + expect = require('chai').expect, + _ = require('underscore'), + Promise = require("bluebird"), + fs = Promise.promisifyAll(require('fs')), + csv = Promise.promisifyAll(require('csv')), + register_new_user_func = require('../lib/register_new_user'), + login_user_func = require('../lib/login_with_user'), + open_page_func = require('../lib/open_page'), + submit_form_func = require('../lib/submit_form'), + add_new_user_func = require('../lib/add_new_user'), + config = require('../lib/config'), + user_info_func = require('../lib/user_info'), + application_host = config.get_application_host(); + +/* + * Scenario to check: + * + * * Register new account + * * Create 10 unique emails/users + * * Put them into CSV and = + * + * */ + + +describe('Bulk import of users', function(){ + + this.timeout( config.get_execution_timeout() ); + + let email_admin, + driver, + csv_data, + test_users_filename = __dirname +'/../test.csv'; + + it('Create new company', function(done){ + register_new_user_func({ + application_host : application_host, + }) + .then(data => { + driver = data.driver; + done(); + }); + }); + + it('Navigate to bulk upload page', function(done){ + open_page_func({ + url : application_host + 'users/import/', + driver : driver, + }) + .then(function(){ done() }); + }); + + it('Create test .CSV file for the test', function(done){ + csv_data = [['email', 'name', 'lastname', 'department']]; + + let token = (new Date()).getTime(); + for (let i=0; i<10; i++){ + csv_data.push([ + 'test_csv_'+i+'_'+token+'@test.com', + 'name_csv_'+i+'_'+token+'@test.com', + 'lastname_csv_'+i+'_'+token+'@test.com', + 'Sales' + ]); + } + + Promise.resolve() + .then(() => fs.unlinkAsync(test_users_filename)) + .catch(err => Promise.resolve()) + .then(() => csv.stringifyAsync( csv_data )) + .then(data => fs.writeFileAsync(test_users_filename, data)) + .then(() => done()); + }); + + it('Upload user import file', function(done){ + let regex = new RegExp( + 'Successfully imported users with following emails: ' + + csv_data.slice(1).map(it => it[0]).sort().join(', ') + ); + + submit_form_func({ + submit_button_selector : '#submit_users_btn', + driver : driver, + form_params : [{ + selector : '#users_input_inp', + value : test_users_filename, + file : true, + }], + message : regex, + }) + .then(() => done()); + }); + + it('Ensure that imported users are in the system', function(done){ + let users_ids; + // Get IDs of newly added users + Promise.map(csv_data.slice(1).map(it => it[0]), email => { + return user_info_func({ + driver : driver, + email : email, + }) + .then(data => data.user.id); + }) + // Open users page + .then(ids => { + users_ids = ids; + + return open_page_func({ + url : application_host + 'users/', + driver : driver, + }); + }) + + // Ensure that IDs of newly added users are on th Users page + .then(() => Promise.map(users_ids, id => driver + .findElement(By.css('[data-vpp-user-row="'+id+'"]')) + .then(el => { + expect(el, 'Ensure that newly added user ID '+id+' exists on Users page') + .to.exists; + return Promise.resolve(); + }) + )) + + .then(() => done()); + }); + + after(function(done){ + driver.quit().then(() => done()); + }); +}); diff --git a/t/lib/add_new_user.js b/t/lib/add_new_user.js index 6b95fbde1..cbacea9b5 100644 --- a/t/lib/add_new_user.js +++ b/t/lib/add_new_user.js @@ -87,7 +87,7 @@ module.exports = Promise.promisify(function(args, callback){ }, select_department, ], - submit_button_selector : add_new_user_form_id+' button[type="submit"]', + submit_button_selector : add_new_user_form_id+' #add_new_user_btn', should_be_successful : error_message ? false : true, elements_to_check : [], message : error_message ? diff --git a/t/lib/submit_form.js b/t/lib/submit_form.js index b6d0d0cee..c41b5413e 100644 --- a/t/lib/submit_form.js +++ b/t/lib/submit_form.js @@ -55,6 +55,9 @@ var submit_form_func = Promise.promisify( function(args, callback){ .then(function(el){ return el.click(); }); } else if ( test_case.hasOwnProperty('tick')) { return el.click(); + } else if (test_case.file) { + return Promise.resolve() + .then(() => el.sendKeys( test_case.value )); } else { return el.clear().then(function(){ el.sendKeys( test_case.value ); diff --git a/views/user_add.hbs b/views/user_add.hbs index 7d69b244f..2bcbe959d 100644 --- a/views/user_add.hbs +++ b/views/user_add.hbs @@ -108,7 +108,7 @@
    - +
    diff --git a/views/users_import.hbs b/views/users_import.hbs index a6010e8d9..6228cb418 100644 --- a/views/users_import.hbs +++ b/views/users_import.hbs @@ -45,7 +45,7 @@
    -
    From 2ecaf1f2816df7f6e8c6fe46883527d2e70797c0 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 07:20:00 +0000 Subject: [PATCH 079/539] Require exact approximately version of csv --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d7345c06..f5eb5b069 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "body-parser": "^1.8.4", "connect-session-sequelize": "3.0.0", "cookie-parser": "^1.3.5", - "csv": "^0.4.6", + "csv": "~0.4.6", "debug": "~2.0.0", "express": "^4.13.4", "express-handlebars": "^3.0.0", From 5ce2c4d01706566e287803ddfb963c08b53197ba Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 09:56:41 +0000 Subject: [PATCH 080/539] Make test to clean up after itself --- t/integration/users_import.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/t/integration/users_import.js b/t/integration/users_import.js index ea3a82591..383225fc3 100644 --- a/t/integration/users_import.js +++ b/t/integration/users_import.js @@ -128,6 +128,10 @@ describe('Bulk import of users', function(){ }); after(function(done){ - driver.quit().then(() => done()); + Promise.resolve() + .then(() => driver.quit()) + .then(() => fs.unlinkAsync(test_users_filename)) + .catch(err => Promise.resolve()) + .then(() => done()); }); }); From 98b34036be5642d8737f0933916d37f73ccb3319 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 09:57:58 +0000 Subject: [PATCH 081/539] Debug travis --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 760f3335e..52f14bc60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,5 @@ cache: env: before_install: before_script: - - node bin/wwww > /dev/null 2>&1 & + # - node bin/wwww > /dev/null 2>&1 & + - node bin/wwww & From c4cac3c4a065fbf82e1aafce17064e1b5d218adf Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 10:16:15 +0000 Subject: [PATCH 082/539] Fix Joi import instruction Linuxes are case sencitive cotrary to Mac OS --- lib/error/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/error/index.js b/lib/error/index.js index 2eeaca536..e95ec15ac 100644 --- a/lib/error/index.js +++ b/lib/error/index.js @@ -2,7 +2,7 @@ "use strict"; const - Joi = require('Joi'); + Joi = require('joi'); const schema_throw_user_error = [ Joi.object() From ed083fb5fe9985dc29580f3b399dbc8d6ab9f258 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 10:17:28 +0000 Subject: [PATCH 083/539] Remove travis debug --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 52f14bc60..760f3335e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,4 @@ cache: env: before_install: before_script: - # - node bin/wwww > /dev/null 2>&1 & - - node bin/wwww & + - node bin/wwww > /dev/null 2>&1 & From 44c7f0ba182ee2fd9a88ae87c8971455a0d82656 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 11:25:24 +0000 Subject: [PATCH 084/539] Clean up UI of new page --- lib/model/user_importer.js | 4 +- t/integration/users_import.js | 4 +- views/settings_company_authentication.hbs | 2 +- views/user_add.hbs | 2 +- views/users_import.hbs | 72 ++++++++++++++--------- 5 files changed, 51 insertions(+), 33 deletions(-) diff --git a/lib/model/user_importer.js b/lib/model/user_importer.js index 0f5c187ee..30a5b6700 100644 --- a/lib/model/user_importer.js +++ b/lib/model/user_importer.js @@ -38,7 +38,7 @@ function add_user(args) { let attributes = {}; - attributes.email = args.email; + attributes.email = args.email.toLowerCase(); attributes.lastname = args.lastname; attributes.name = args.name; attributes.companyId = args.company_id; @@ -113,7 +113,7 @@ function add_users_in_bulk(args) { .map(vector => '"'+vector[department_vector_index]+'"') .join(', '); - Error.throw_user_error({ + Exception.throw_user_error({ user_error : 'Following departments could not be found in ' + company.name + ' account: ' + unknown_departments, system_error : 'While importing users to company ' diff --git a/t/integration/users_import.js b/t/integration/users_import.js index 383225fc3..c543b2b79 100644 --- a/t/integration/users_import.js +++ b/t/integration/users_import.js @@ -22,7 +22,9 @@ var test = require('selenium-webdriver/testing'), * * * Register new account * * Create 10 unique emails/users - * * Put them into CSV and = + * * Put them into CSV and import in bulk + * * Ensure that all users were added into + * system and they appear on the Users page * * */ diff --git a/views/settings_company_authentication.hbs b/views/settings_company_authentication.hbs index 115678e0e..11f9cff3f 100644 --- a/views/settings_company_authentication.hbs +++ b/views/settings_company_authentication.hbs @@ -13,7 +13,7 @@
    -

    TimeOff.Management supports LDAP authentication for customers that want to integrate application with the rest of their infrastructure. The obvious reason is to allow employees reuse their Active directory credentials in TimeOff.Management..

    +

    TimeOff.Management supports LDAP authentication for customers that want to integrate application with the rest of their infrastructure. The obvious reason is to allow employees reuse their Active directory credentials in TimeOff.Management.

    This page allows you to setup the TimeOff.Management to communicate with a custom LDAP server.

    diff --git a/views/user_add.hbs b/views/user_add.hbs index 2bcbe959d..09b078aba 100644 --- a/views/user_add.hbs +++ b/views/user_add.hbs @@ -8,7 +8,7 @@
    Adding new employee account
    diff --git a/views/users_import.hbs b/views/users_import.hbs index 6228cb418..cfcca9ba9 100644 --- a/views/users_import.hbs +++ b/views/users_import.hbs @@ -3,56 +3,72 @@

    Employees bulk import

    -{{> show_flash_messages }} -
    Quick employees import
    -
     
    +{{> show_flash_messages }} + +
    +

    Description

    +
    +
    +

    TimeOff.Management supports importing employees in a bulk. So configuring the company account is fast and straightforward..

    -
    +

    In order to import employees into the system one needs to build .CSV file of following columns (in exact order):

    -

    Download sample

    +
      +
    • email: email of employee to be added
    • +
    • name: employee's name
    • +
    • lastname: employee's last name
    • +
    • department: name of the department new employee is going to be part of. Please ensure that the department is already added into Company account.
    • +
    -
    -
    - -
    - -
    -
    -
    -
    - Download sample prepopulated with current employees. -
    -
    -
    +

    The easiest way to get import file is to download sample one based on current settings of company account from the form below. Amend the file with new employees and upload the file in the form below.

    +

    Users who are already in the system are ignored during import.

    +
    +
    -
    +
    +

    Import

    +
    +
    + +
    +
    - -
    - -
    + +
    -
    -
    - -
    +
    +
    +
    + +
    +
    +
    + The sample file contains information about employees who are already in the system. +
    +
    + Extend the file to have records about new employees and ensure upload it from the form on left hand side. +
    +
    +
    +
     
    From 3d56938e43e2ebd6f0de56a493b1b96fd0ed3833 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 11:56:54 +0000 Subject: [PATCH 085/539] Sort out links --- views/partials/header.hbs | 1 + views/user_add.hbs | 2 +- views/users.hbs | 7 ++++--- views/users_import.hbs | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/views/partials/header.hbs b/views/partials/header.hbs index f7dce82ef..d21614cfc 100644 --- a/views/partials/header.hbs +++ b/views/partials/header.hbs @@ -40,6 +40,7 @@
  • Departments
  • LDAP configuration
  • Emails audit
  • +
  • Import employees
  • {{/if}} diff --git a/views/user_add.hbs b/views/user_add.hbs index 09b078aba..a76f893a7 100644 --- a/views/user_add.hbs +++ b/views/user_add.hbs @@ -8,7 +8,7 @@
    Adding new employee account
    diff --git a/views/users.hbs b/views/users.hbs index 08d36497a..773104a0f 100644 --- a/views/users.hbs +++ b/views/users.hbs @@ -9,13 +9,14 @@
    {{company.name}}'s staff
    - Add new employee -
    diff --git a/views/users_import.hbs b/views/users_import.hbs index cfcca9ba9..40b0d2114 100644 --- a/views/users_import.hbs +++ b/views/users_import.hbs @@ -6,7 +6,7 @@
    Quick employees import
    @@ -64,7 +64,7 @@ The sample file contains information about employees who are already in the system.
    - Extend the file to have records about new employees and ensure upload it from the form on left hand side. + Extend the file to have records about new employees and upload it from the form on left hand side.
    From 93f74e4f5d7ad7994abad8d9506398d9c9a47a25 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 12:22:35 +0000 Subject: [PATCH 086/539] Amend tests to respect new page structure --- t/lib/add_new_user.js | 128 ++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 74 deletions(-) diff --git a/t/lib/add_new_user.js b/t/lib/add_new_user.js index cbacea9b5..5d59b7d92 100644 --- a/t/lib/add_new_user.js +++ b/t/lib/add_new_user.js @@ -30,82 +30,62 @@ module.exports = Promise.promisify(function(args, callback){ var new_user_email = args.email || random_token + '@test.com'; // Open front page - driver.get( application_host ); - - driver.findElement( By.css('a[href="/users/"]') ) - .then(function(el){ - return el.getText(); - }) - .then(function(text){ - expect(text).to.be.equal('Employees'); + driver.get( application_host + 'users/add/'); + + + driver.call(() => { + + var select_department = {}; + if (typeof department_index !== 'undefined') { + + select_department = { + selector : 'select[name="department"]', + option_selector : 'option[data-vpp="'+department_index+'"]', + }; + } + + return submit_form_func({ + driver : driver, + form_params : [{ + selector : add_new_user_form_id+' input[name="name"]', + value : 'name'+random_token, + },{ + selector : add_new_user_form_id+' input[name="lastname"]', + value : 'lastname'+random_token, + },{ + selector : add_new_user_form_id+' input[name="email_address"]', + value : new_user_email, + },{ + selector : add_new_user_form_id+' input[name="password_one"]', + value : '123456', + },{ + selector : add_new_user_form_id+' input[name="password_confirm"]', + value : '123456', + },{ + selector : add_new_user_form_id+' input[name="start_date"]', + value : '2015-06-01', + }, + select_department, + ], + submit_button_selector : add_new_user_form_id+' #add_new_user_btn', + should_be_successful : error_message ? false : true, + elements_to_check : [], + message : error_message ? + new RegExp(error_message) : + /New user account successfully added/, }); - - driver.findElement( By.css('a[href="/users/"]') ) - .then(function(el){ - return el.click(); - }); - - driver.wait(until.elementLocated(By.css('#add_new_department')), 1000); - - driver.findElement( By.css('#add_new_department') ) - .then(function(el){ - return el.click(); - }) - .then(function(){ - - driver.wait(until.elementLocated(By.css('input[name="name"]')), 1000); - - var select_department = {}; - if (typeof department_index !== 'undefined') { - - select_department = { - selector : 'select[name="department"]', - option_selector : 'option[data-vpp="'+department_index+'"]', - }; + }) + + driver.call(function(){ + // "export" + result_callback( + null, + { + driver : driver, + new_user_email : new_user_email, } - - return submit_form_func({ - driver : driver, - form_params : [{ - selector : add_new_user_form_id+' input[name="name"]', - value : 'name'+random_token, - },{ - selector : add_new_user_form_id+' input[name="lastname"]', - value : 'lastname'+random_token, - },{ - selector : add_new_user_form_id+' input[name="email_address"]', - value : new_user_email, - },{ - selector : add_new_user_form_id+' input[name="password_one"]', - value : '123456', - },{ - selector : add_new_user_form_id+' input[name="password_confirm"]', - value : '123456', - },{ - selector : add_new_user_form_id+' input[name="start_date"]', - value : '2015-06-01', - }, - select_department, - ], - submit_button_selector : add_new_user_form_id+' #add_new_user_btn', - should_be_successful : error_message ? false : true, - elements_to_check : [], - message : error_message ? - new RegExp(error_message) : - /New user account successfully added/, - }); - }) - - .then(function(){ - // "export" - result_callback( - null, - { - driver : driver, - new_user_email : new_user_email, - } - ); - }); + ); + }); }); From 5246764a18dc9745ba5e0cf62c928d6ec15e96c6 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Wed, 24 Jan 2018 06:20:48 +0000 Subject: [PATCH 087/539] Add menu separators to group items by nature --- views/partials/header.hbs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/views/partials/header.hbs b/views/partials/header.hbs index d21614cfc..e905aee4f 100644 --- a/views/partials/header.hbs +++ b/views/partials/header.hbs @@ -37,10 +37,12 @@ {{/if}} @@ -51,6 +53,7 @@ From dbb1f84ce4edf72034fe4c7583650d8cdac82363 Mon Sep 17 00:00:00 2001 From: Pavlo Date: Thu, 25 Jan 2018 06:57:31 +0000 Subject: [PATCH 088/539] Update version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f5eb5b069..b58cde173 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "TimeOff.Management", - "version": "0.7.0", + "version": "0.10.0", "private": false, "description": "Simple yet powerful absence management software for small and medium size business", "engines": { From 268c946ac7285217f0ee30618912f8e1ea96196e Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 25 Jan 2018 16:25:08 +0000 Subject: [PATCH 089/539] Refactore logic behind leave revoking to be more efficient --- lib/route/requests.js | 139 ++++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 67 deletions(-) diff --git a/lib/route/requests.js b/lib/route/requests.js index e7585671f..a6af6c414 100644 --- a/lib/route/requests.js +++ b/lib/route/requests.js @@ -189,91 +189,96 @@ router.post( } if ( req.session.flash_has_errors() ) { - console.error( + console.log( 'Got validation errors when revoking leave request for user ' + req.user.id ); return res.redirect_with_session('../'); } - Promise.try(function(){ - return Promise.join( - // Get user's leaves - req.user.promise_my_active_leaves_ever(), - // Get all leaves from users supervised by current user - req.user + Promise + // Get the Leave object for submitted ID + .try(() => req.app.get('db_model').Leave.findOne({ where : { id : request_id }})) + + // Ensure that current user can act on this Leave object + .then(requested_leave => { + + // Case when requested Leave is originated from current user + if ( String(requested_leave.userId) === String(req.user.id) ) { + return Promise.resolve( requested_leave ) + } + + // Case when requested Leave is originated from one of employees + // current user can manage + return req.user .promise_users_I_can_manage() - .then(function(users){ - return Promise.all( - _.map( - users, - function(user){ - return user.promise_my_active_leaves_ever(); - } - )) - .then(function(array_of_leave_arrays){ - return Promise.resolve(_.flatten(array_of_leave_arrays, true)); - }); - }), - function(users_leaves, supervised_leaves){ - // Compose all leaves into one new array - return Promise.resolve(users_leaves.concat(supervised_leaves)); - }); - }) - .then(function(leaves){ - var leave_to_process = _.find(leaves, function(leave){ - return String(leave.id) === String(request_id) - && leave.is_approved_leave(); - }); + .then(users => { + if ( users.find(u => String(u.id) === String(requested_leave.userId)) ) { + return Promise.resolve( requested_leave ); + } - if (! leave_to_process) { - throw new Error('Provided ID '+request_id - +' does not correspond to any leave requests to be revoked by user ' - + req.user.id - ); - } + return Promise.resolve(); + }); + }) - return leave_to_process.promise_to_revoke(); - }) - .then(function(processed_leave){ - return processed_leave.reload({ + .then(leave_to_process => { + + // Ensure that Leave is in status from it could be revoked + if (! leave_to_process) { + throw new Error('Provided ID '+request_id + +' does not correspond to any leave requests to be revoked by user ' + + req.user.id + ); + } + + // Do the action + return leave_to_process.promise_to_revoke(); + }) + + // Ensure that Leave object has all content necessary for sending emails + .then(processed_leave => processed_leave.reload({ include : [ {model : req.app.get('db_model').User, as : 'user'}, {model : req.app.get('db_model').User, as : 'approver'}, {model : req.app.get('db_model').LeaveType, as : 'leave_type' }, ], - }); - }) - .then(function(processed_leave){ + })) - var Email = new EmailTransport(); + // Send relevant emails + .then(processed_leave => { - return Email.promise_leave_request_revoke_emails({ - leave : processed_leave, + var Email = new EmailTransport(); + + return Email.promise_leave_request_revoke_emails({ + leave : processed_leave, + }) + .then(function(){ + return Promise.resolve(processed_leave); + }); }) - .then(function(){ - return Promise.resolve(processed_leave); - }); - }) - .then(function(processed_leave){ - req.session.flash_message( - 'You have requested leave to be revoked. ' - + ( - processed_leave.user.is_auto_approve() - ? '' - : 'Your supervisor needs to approve it' - ) - ); - return res.redirect_with_session('../'); - }) - .catch(function(error){ - console.error('An error occurred when attempting to revoke leave request ' - +request_id+' by user '+req.user.id+' Error: '+error - ); - req.session.flash_error('Failed to revoke leave request'); - return res.redirect_with_session('../'); - }); + // Deal with next page: where to land and what to show + .then(processed_leave => { + req.session.flash_message( + 'You have requested leave to be revoked. ' + + ( + processed_leave.user.is_auto_approve() + ? '' + : 'Your supervisor needs to approve it' + ) + ); + + return res.redirect_with_session('../'); + }) + + // Deal with issues if any occurs + .catch(error => { + console.error('An error occurred when attempting to revoke leave request ' + +request_id+' by user '+req.user.id+' Error: '+error + ); + req.session.flash_error('Failed to revoke leave request'); + return res.redirect_with_session('../'); + }); } ); From a3432b3c090f7d5f0bdd7598e769c90194fffbf8 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Fri, 26 Jan 2018 07:51:10 +0000 Subject: [PATCH 090/539] Make action button more prominant --- views/user_add.hbs | 2 +- views/users.hbs | 2 +- views/users_import.hbs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/views/user_add.hbs b/views/user_add.hbs index a76f893a7..d7dda5e27 100644 --- a/views/user_add.hbs +++ b/views/user_add.hbs @@ -8,7 +8,7 @@
    Adding new employee account
    diff --git a/views/users.hbs b/views/users.hbs index 773104a0f..a1d632b76 100644 --- a/views/users.hbs +++ b/views/users.hbs @@ -9,7 +9,7 @@
    {{company.name}}'s staff
    -
    +
    + + + +
    From ee5bc31a7f9fbb0653dbe794cc4f063688b9d0f2 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Sat, 27 Jan 2018 09:17:58 +0000 Subject: [PATCH 092/539] Restructure code so logic is little bit more structureal way Still there is fat controller --- lib/model/db/company.js | 2 +- lib/route/users.js | 56 ++++++++++++++++++++++++++++------------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/lib/model/db/company.js b/lib/model/db/company.js index 5ee127975..fa8296bbb 100644 --- a/lib/model/db/company.js +++ b/lib/model/db/company.js @@ -339,7 +339,7 @@ module.exports = function(sequelize, DataTypes) { * * */ name_for_machine : function(){ - return this.name.replace(/\s+/, '_'); + return this.name.replace(/\s+/g, '_'); }, reload_with_bank_holidays : function(){ diff --git a/lib/route/users.js b/lib/route/users.js index 1bb0e9606..110e32aba 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -804,25 +804,12 @@ router.get('/', function(req, res) { users_info = args[1]; if ( req.param('as-csv') ) { - res.attachment( - company.name_for_machine()+'_employees_on_'+company.get_today().format('YYYY_MMM_DD')+'.csv' - ); - - let content = [['email', 'lastname', 'name', 'department', 'remaining allowance', 'days used']]; - - users_info.forEach(ui => { - content.push([ - ui.user_row.email, - ui.user_row.lastname, - ui.user_row.name, - ui.user_row.department.name, - ui.number_of_days_available_in_allowance, - ui.user_row.calculate_number_of_days_taken_from_allowance() - ]); + return users_list_as_csv({ + users_info : users_info, + company : company, + req : res, + res : res, }); - - return csv.stringifyAsync( content ) - .then(csv_data_string => res.send(csv_data_string)); } res.render('users', { @@ -834,6 +821,39 @@ router.get('/', function(req, res) { }); }); +function users_list_as_csv(args) { + let users_info = args.users_info, + company = args.company, + req = args.req, + res = args.res; + + // Compose file name + res.attachment( + company.name_for_machine() + + '_employees_on_' + + company.get_today().format('YYYY_MMM_DD') + + '.csv' + ); + + // Compose result CSV header + let content = [['email', 'lastname', 'name', 'department', 'remaining allowance', 'days used']]; + + // ... and body + users_info.forEach(ui => { + content.push([ + ui.user_row.email, + ui.user_row.lastname, + ui.user_row.name, + ui.user_row.department.name, + ui.number_of_days_available_in_allowance, + ui.user_row.calculate_number_of_days_taken_from_allowance() + ]); + }); + + return csv.stringifyAsync( content ) + .then(csv_data_string => res.send(csv_data_string)); +} + function get_and_validate_user_parameters(args) { var req = args.req, item_name = args.item_name, From ec4f588741353fa35b1535b0945f99ed5aaa5822 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Sat, 27 Jan 2018 10:00:29 +0000 Subject: [PATCH 093/539] Slowly lighten template from complex objects --- lib/route/users.js | 39 ++++++++++++++++++++++++++++++++++----- views/users.hbs | 14 ++++++-------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/lib/route/users.js b/lib/route/users.js index 110e32aba..5e4b7ae83 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -798,6 +798,14 @@ router.get('/', function(req, res) { .then(users_info => Promise.resolve([company, users_info])) ) + // We are moving away from passing complex objects into templates + // for callting complicated methods from within templates + // Now only basic simple objects to be sent over to tamples, + // all preparation to be done before rendering. + // + // So prepare special rendering datastructure here + .then(args => promise_user_list_data_for_rendering(args)) + .then(function(args){ let company = args[0], @@ -821,6 +829,27 @@ router.get('/', function(req, res) { }); }); +function promise_user_list_data_for_rendering(args) { + let + company = args[0], + users_info = args[1]; + + let users_info_for_rendering = users_info.map(ui => ({ + user_id : ui.user_row.id, + user_email : ui.user_row.email, + user_name : ui.user_row.name, + user_lastname : ui.user_row.lastname, + user_full_name : ui.user_row.full_name(), + department_id : ui.user_row.department.id, + department_name : ui.user_row.department.name, + is_admin : ui.user_row.admin, + number_of_days_available_in_allowance : ui.number_of_days_available_in_allowance, + number_of_days_taken_from_allowance : ui.user_row.calculate_number_of_days_taken_from_allowance(), + })); + + return Promise.resolve([company, users_info_for_rendering]); +}; + function users_list_as_csv(args) { let users_info = args.users_info, company = args.company, @@ -841,12 +870,12 @@ function users_list_as_csv(args) { // ... and body users_info.forEach(ui => { content.push([ - ui.user_row.email, - ui.user_row.lastname, - ui.user_row.name, - ui.user_row.department.name, + ui.user_email, + ui.user_lastname, + ui.user_name, + ui.department_name, ui.number_of_days_available_in_allowance, - ui.user_row.calculate_number_of_days_taken_from_allowance() + ui.number_of_days_taken_from_allowance ]); }); diff --git a/views/users.hbs b/views/users.hbs index 1e8c1b091..87ee69d5f 100644 --- a/views/users.hbs +++ b/views/users.hbs @@ -49,15 +49,13 @@
    {{this.department.name}}{{# if this.admin }}Yes{{else}}{{/if}}{{ ../number_of_days_available_in_allowance }}{{this.calculate_number_of_days_taken_from_allowance}}
    {{this.department_name}}{{# if this.is_admin }}Yes{{else}}{{/if}}{{ this.number_of_days_available_in_allowance }}{{this.number_of_days_taken_from_allowance}}
    From 71096b3e3cb78d2bb48e58cc31cf4c373e07efc8 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Sat, 27 Jan 2018 10:00:29 +0000 Subject: [PATCH 094/539] Slowly lighten template from complex objects --- lib/route/users.js | 39 ++++++++++++++++++++++++++++++++++----- views/users.hbs | 14 ++++++-------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/lib/route/users.js b/lib/route/users.js index 110e32aba..5e4b7ae83 100644 --- a/lib/route/users.js +++ b/lib/route/users.js @@ -798,6 +798,14 @@ router.get('/', function(req, res) { .then(users_info => Promise.resolve([company, users_info])) ) + // We are moving away from passing complex objects into templates + // for callting complicated methods from within templates + // Now only basic simple objects to be sent over to tamples, + // all preparation to be done before rendering. + // + // So prepare special rendering datastructure here + .then(args => promise_user_list_data_for_rendering(args)) + .then(function(args){ let company = args[0], @@ -821,6 +829,27 @@ router.get('/', function(req, res) { }); }); +function promise_user_list_data_for_rendering(args) { + let + company = args[0], + users_info = args[1]; + + let users_info_for_rendering = users_info.map(ui => ({ + user_id : ui.user_row.id, + user_email : ui.user_row.email, + user_name : ui.user_row.name, + user_lastname : ui.user_row.lastname, + user_full_name : ui.user_row.full_name(), + department_id : ui.user_row.department.id, + department_name : ui.user_row.department.name, + is_admin : ui.user_row.admin, + number_of_days_available_in_allowance : ui.number_of_days_available_in_allowance, + number_of_days_taken_from_allowance : ui.user_row.calculate_number_of_days_taken_from_allowance(), + })); + + return Promise.resolve([company, users_info_for_rendering]); +}; + function users_list_as_csv(args) { let users_info = args.users_info, company = args.company, @@ -841,12 +870,12 @@ function users_list_as_csv(args) { // ... and body users_info.forEach(ui => { content.push([ - ui.user_row.email, - ui.user_row.lastname, - ui.user_row.name, - ui.user_row.department.name, + ui.user_email, + ui.user_lastname, + ui.user_name, + ui.department_name, ui.number_of_days_available_in_allowance, - ui.user_row.calculate_number_of_days_taken_from_allowance() + ui.number_of_days_taken_from_allowance ]); }); diff --git a/views/users.hbs b/views/users.hbs index 1e8c1b091..87ee69d5f 100644 --- a/views/users.hbs +++ b/views/users.hbs @@ -49,15 +49,13 @@
    {{this.department.name}}{{# if this.admin }}Yes{{else}}{{/if}}{{ ../number_of_days_available_in_allowance }}{{this.calculate_number_of_days_taken_from_allowance}}
    {{this.department_name}}{{# if this.is_admin }}Yes{{else}}{{/if}}{{ this.number_of_days_available_in_allowance }}{{this.number_of_days_taken_from_allowance}}
    From f76984d1f2066d04dc42802a9f79c322e904388c Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Sat, 27 Jan 2018 10:18:37 +0000 Subject: [PATCH 095/539] Add tooltip for newly added .csv button on users page --- views/users.hbs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/views/users.hbs b/views/users.hbs index 87ee69d5f..ca9105fea 100644 --- a/views/users.hbs +++ b/views/users.hbs @@ -22,7 +22,14 @@
    - +
    From 9d7817523b8aa45f306624584384fe4d9b56bbf4 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Sat, 27 Jan 2018 10:49:29 +0000 Subject: [PATCH 096/539] Add some navigation links to user add/import pages --- views/user_add.hbs | 209 ++++++++++++++++++++++------------------- views/users_import.hbs | 12 ++- 2 files changed, 124 insertions(+), 97 deletions(-) diff --git a/views/user_add.hbs b/views/user_add.hbs index d7dda5e27..c47bddc34 100644 --- a/views/user_add.hbs +++ b/views/user_add.hbs @@ -3,8 +3,6 @@

    New employee

    -{{> show_flash_messages }} -
    Adding new employee account
    @@ -12,105 +10,124 @@
    -
     
    - -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    - Email address used by employee -
    - -
    - -
    - -
    - Department employee belongs to -
    - -
    -
    - -
    -
    - -
    -
    - -
    - Set the flag ON to streamline leave requests from this employee directly into Approved state. -
    - -
    - -
    - -
    - Date when employee started (inclusive) -
    +{{> show_flash_messages }} -
    - -
    - -
    -
    Date when user quits the {{ company.name }}.
    After this date user is not able to access the company.
    This is a way of deactivating user accounts.
    +
    +
    +
    +
    -
    - -
    - -
    - - {{# if company.ldap_auth_enabled }} - LDAP authentication is enabled, so that password does not matter. - {{else}} - Define employee's password. - {{/ if}} - -
    +
    +

    Details of new employee

    +
    -
    - -
    - -
    -
    +
    +
    + + + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    + Email address used by employee +
    + +
    + +
    + +
    + Department employee belongs to +
    + +
    +
    + +
    +
    + +
    +
    + +
    + Set the flag ON to streamline leave requests from this employee directly into Approved state. +
    + +
    + +
    + +
    + Date when employee started (inclusive) +
    + +
    + +
    + +
    +
    Date when user quits the {{ company.name }}.
    After this date user is not able to access the company.
    This is a way of deactivating user accounts.
    +
    + +
    + +
    + +
    + + {{# if company.ldap_auth_enabled }} + LDAP authentication is enabled, so that password does not matter. + {{else}} + Define employee's password. + {{/ if}} + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + -
    -
    - -
    - +
    {{> footer }} diff --git a/views/users_import.hbs b/views/users_import.hbs index 68353ce6e..1c4119ad5 100644 --- a/views/users_import.hbs +++ b/views/users_import.hbs @@ -8,17 +8,27 @@ +
    {{> show_flash_messages }} +
    +
    + +
    +
    +

    Description

    -

    TimeOff.Management supports importing employees in a bulk. So configuring the company account is fast and straightforward..

    +

    TimeOff.Management supports importing employees in a bulk.

    In order to import employees into the system one needs to build .CSV file of following columns (in exact order):

    From 664ced15982f728fa6fe1d42f7d68e4caa98785b Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Sat, 27 Jan 2018 11:00:29 +0000 Subject: [PATCH 097/539] Add hovering effect to team view page table --- views/team_view.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/team_view.hbs b/views/team_view.hbs index 56062bb76..78472909d 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -31,7 +31,7 @@
    - +
    From 4ef81b9b80ff3208b750fcd9187a33c9816385f9 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Tue, 23 Jan 2018 14:14:52 +0000 Subject: [PATCH 098/539] Add sketches of reports index page --- app.js | 5 +++++ lib/route/reports.js | 20 ++++++++++++++++++++ views/report/index.hbs | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 lib/route/reports.js create mode 100644 views/report/index.hbs diff --git a/app.js b/app.js index c0155a27d..3157ba07e 100644 --- a/app.js +++ b/app.js @@ -141,6 +141,11 @@ app.use( require('./lib/route/audit') ); +app.use( + '/reports/', + require('./lib/route/reports') +); + // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); diff --git a/lib/route/reports.js b/lib/route/reports.js new file mode 100644 index 000000000..67d6973ee --- /dev/null +++ b/lib/route/reports.js @@ -0,0 +1,20 @@ + +"use strict"; + +const + express = require('express'), + router = express.Router(), + validator = require('validator'), + Promise = require('bluebird'), + moment = require('moment'), + config = require('../config'), + _ = require('underscore'); + +// Make sure that current user is authorized to deal with settings +router.all(/.*/, require('../middleware/ensure_user_is_admin')); + +router.get('/', (req, res) => { + res.render('report/index'); +}); + +module.exports = router; diff --git a/views/report/index.hbs b/views/report/index.hbs new file mode 100644 index 000000000..1a12ae8e7 --- /dev/null +++ b/views/report/index.hbs @@ -0,0 +1,18 @@ + +{{> header }} + +

    Reports

    + +
    +
    List of available reports
    +
    + +
     
    + + + +{{> footer }} From 328791040f59678114b8e0c4b392d6c005354b5c Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Thu, 25 Jan 2018 06:55:39 +0000 Subject: [PATCH 099/539] Some hardcoded report implementation --- lib/model/db/department.js | 108 ++++++++++++++++++++++++------- lib/model/team_view.js | 52 +++++++++++---- lib/route/reports.js | 52 +++++++++++++++ views/partials/header.hbs | 1 + views/report/allowancebytime.hbs | 64 ++++++++++++++++++ views/report/index.hbs | 6 +- views/team_view.hbs | 1 + 7 files changed, 244 insertions(+), 40 deletions(-) create mode 100644 views/report/allowancebytime.hbs diff --git a/lib/model/db/department.js b/lib/model/db/department.js index 99e531802..48666ff57 100644 --- a/lib/model/db/department.js +++ b/lib/model/db/department.js @@ -95,23 +95,67 @@ module.exports = function(sequelize, DataTypes) { }); }, - promise_team_view : function(args){ + promise_team_view_for_month : function( month ){ + return this._promise_team_view({ start_date : month }); + }, + + promise_team_view_for_months_range : function (start_month, end_month){ + return this._promise_team_view({ + start_date : start_month, + end_date : end_month, + }); + }, + + _promise_team_view : function(args){ - var self = this, - model = sequelize.models, - base_date = args.base_date; + let + self = this, + model = sequelize.models, + start_date = args.start_date, + end_date = args.end_date; var promise_users_and_leaves = Promise - // First of all ensure that "base_date" is defined + // First of all ensure that "start_date" is defined .try(function(){ - if ( base_date ) { - return Promise.resolve(base_date); + if ( start_date ) { + return Promise.resolve(start_date); } return self.getCompany() - .then(company => Promise.resolve( base_date = company.get_today() ) ) + .then(company => Promise.resolve( start_date = company.get_today() ) ) + }) + + // Ensure end_date is suitable if it was provided + .then(() => { + + // If end_date was not provided: no need to validate it: set it to be equal to start date + if ( ! end_date ) { + + end_date = start_date; + + return Promise.resolve(); + } + + // If end date is privided... + // ... ensure start and end dates are from within same year + if (moment.utc(end_date).format('YYYY') !== moment.utc(start_date).format('YYYY')) { + Exception.throw_user_error({ + user_error : 'Start and End dates should within single year', + system_error : '_promise_team_view was called with start_date and end_date from different years.', + }); + } + + // ... ensure that start date proceed end date + if (moment.utc(start_date).dayOfYear() > moment.utc(end_date).dayOfYear()) { + Exception.throw_user_error({ + user_error : 'Start date needs to be before end date', + system_error : '_promise_team_view was called with end_date prior to start_date', + }); + } + + return Promise.resolve(); }) // Get users @@ -123,7 +167,7 @@ module.exports = function(sequelize, DataTypes) { users, function(user){ return user.promise_my_leaves_for_calendar({ - year : base_date, + year : start_date, }) .then(function(leaves){ @@ -159,21 +203,37 @@ module.exports = function(sequelize, DataTypes) { promise_company, promise_users_and_leaves, function(company, users_and_leaves){ - _.each(users_and_leaves, function(user_data){ - var calendar_month = new CalendarMonth(base_date,{ - bank_holidays : - self.include_public_holidays - ? _.map( - company.bank_holidays, - function(day){return day.date} - ) - : [], - leave_days : user_data.leave_days, - schedule : user_data.schedule, - today : company.get_today(), - }); - - user_data.days = calendar_month.as_for_team_view(); + + let number_of_months = moment.utc(end_date).month() - moment.utc(start_date).month(); + + users_and_leaves.forEach( user_data => { + + user_data.days = []; + + // Now iterate throw all monthes between start and end dates + // and get calendar months for each + // and then combined them all togather + for ( let i=0; i<=number_of_months; i++ ) { + + let calendar_month = new CalendarMonth( + moment.utc(start_date).clone().add(i, 'months'), + { + bank_holidays : + self.include_public_holidays + ? _.map( + company.bank_holidays, + function(day){return day.date} + ) + : [], + leave_days : user_data.leave_days, + schedule : user_data.schedule, + today : company.get_today(), + } + ); + + user_data.days.push( calendar_month.as_for_team_view()); + } // end of for + user_data.days = _.flatten( user_data.days ); }); return Promise.resolve(users_and_leaves); diff --git a/lib/model/team_view.js b/lib/model/team_view.js index 90d11d6aa..233a3131e 100644 --- a/lib/model/team_view.js +++ b/lib/model/team_view.js @@ -2,16 +2,36 @@ 'use strict'; const - moment = require('moment'), - Promise = require('bluebird'), - Joi = require('joi'), - _ = require('underscore'); + moment = require('moment'), + Promise = require('bluebird'), + Joi = require('joi'), + Exception = require('../error'), + _ = require('underscore'); function TeamView(args) { var me = this; this.user = args.user; this.base_date = args.base_date || this.user.company.get_today(); + + // Optional parameters that override base date specify months range + // Team view is going to represent. + // + // The precision is up to month, that is any smaller part of dates + // (such as days, hours etc) are ignored. + // + // If those two parameters are missed - base_date is used to determine with month + // Team view would represent. + // + this.start_date = args.start_date; + this.end_date = args.end_date; + + if (args.start_date && args.end_date && args.base_date) { + Exception.throw_user_error({ + user_error : 'Failed to calculate team view', + system_error : 'TeamView could not be instanciated with start_date, end_data and base_date all defined.' + }); + } } TeamView.prototype.promise_team_view_details = function(args){ @@ -21,11 +41,13 @@ TeamView.prototype.promise_team_view_details = function(args){ args = {}; } - var user = this.user, - current_department_id = args.department_id, // optional parameter - related_departments = [], - current_department, - base_date = this.base_date; + let + self = this, + user = this.user, + current_department_id = args.department_id, // optional parameter + related_departments = [], + current_department, + base_date = this.base_date; var promise_departments; @@ -69,15 +91,17 @@ TeamView.prototype.promise_team_view_details = function(args){ } // Calculate users and leaves for every department - var promise_users_and_leaves = promise_departments.map(function(department){ - return department.promise_team_view({ base_date : base_date }); - }); + var promise_users_and_leaves = promise_departments + .map(department => self.start_date && self.end_date + ? department.promise_team_view_for_months_range( self.start_date, self.end_date ) + : department.promise_team_view_for_month( base_date ) + ); - return promise_users_and_leaves.then(function(users_and_leaves){ + return promise_users_and_leaves.then(users_and_leaves => { users_and_leaves = _.sortBy( _.flatten(users_and_leaves), - function(item){ return item.user.lastname + item.user.name; } + item => item.user.lastname + item.user.name ); return Promise.resolve({ diff --git a/lib/route/reports.js b/lib/route/reports.js index 67d6973ee..fdb29c787 100644 --- a/lib/route/reports.js +++ b/lib/route/reports.js @@ -8,6 +8,7 @@ const Promise = require('bluebird'), moment = require('moment'), config = require('../config'), + TeamView = require('../model/team_view'), _ = require('underscore'); // Make sure that current user is authorized to deal with settings @@ -17,4 +18,55 @@ router.get('/', (req, res) => { res.render('report/index'); }); +router.get('/allowancebytime/', (req, res) => { + + var base_date = validator.isDate(req.param('date')) + ? moment.utc(req.param('date')) + : req.user.company.get_today(); + + var team_view = new TeamView({ +// base_date : base_date, + user : req.user, + start_date : base_date.clone().add(-5, 'months'), + end_date : base_date.clone().add(-1, 'months'), + }); + + var current_deparment_id = validator.isNumeric(req.param('department')) + ? req.param('department') + : null; + + Promise.join( + team_view.promise_team_view_details({ + department_id : current_deparment_id, + }), + req.user.get_company_with_all_leave_types(), + (team_view_details, company) => { + team_view + .inject_statistics({ + team_view_details : team_view_details, + leave_types : company.leave_types, + }) + .then(team_view_details => res.render('report/allowancebytime', { + users_and_leaves : team_view_details.users_and_leaves, + related_departments : team_view_details.related_departments, + current_department : team_view_details.current_department, + company : company, + }) + ); + }) + .catch(error => { + console.error( + 'An error occured when user '+req.user.id+ + ' tried to access Teamview page: '+error + ); + req.session.flash_error('Failed to access Teamview page. Please contact administrator.'); + if (error.hasOwnProperty('user_message')) { + req.session.flash_error(error.user_message); + } + return res.redirect_with_session('/'); + }); + + // res.render('report/allowancebytime'); +}); + module.exports = router; diff --git a/views/partials/header.hbs b/views/partials/header.hbs index e905aee4f..9ec2f407b 100644 --- a/views/partials/header.hbs +++ b/views/partials/header.hbs @@ -43,6 +43,7 @@
  • Import employees
  • Emails audit
  • +
  • Reports
  • {{/if}} diff --git a/views/report/allowancebytime.hbs b/views/report/allowancebytime.hbs new file mode 100644 index 000000000..4f959e5ca --- /dev/null +++ b/views/report/allowancebytime.hbs @@ -0,0 +1,64 @@ + +{{> header }} + +

    Allowance usage by time

    + +
    +
    Shows allowance usage in given time frame
    +
    + + + +
     
    + +
    +
    +

    Here will users

    +
    +
    + +
    +
    + + + + + + + + {{# each users_and_leaves}} + + + + + {{/each}} +
    + +
    + + {{ statistics.deducted_days }} + +
    +
    +
    + +{{> footer }} diff --git a/views/report/index.hbs b/views/report/index.hbs index 1a12ae8e7..714276634 100644 --- a/views/report/index.hbs +++ b/views/report/index.hbs @@ -10,8 +10,10 @@
     
    - diff --git a/views/team_view.hbs b/views/team_view.hbs index 78472909d..d9756ac8c 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -76,6 +76,7 @@ {{/each}}
    +
     
    From ecdad755188e424fa786c2ea546dc7a189ecbed3 Mon Sep 17 00:00:00 2001 From: Pavlo Vodopyan Date: Sat, 27 Jan 2018 13:47:05 +0000 Subject: [PATCH 100/539] Some ideas about UI for report page --- lib/route/reports.js | 13 +++--- views/audit/emails.hbs | 6 +-- views/report/allowancebytime.hbs | 75 +++++++++++++++++++++----------- views/team_view.hbs | 1 + 4 files changed, 61 insertions(+), 34 deletions(-) diff --git a/lib/route/reports.js b/lib/route/reports.js index fdb29c787..e2f600f54 100644 --- a/lib/route/reports.js +++ b/lib/route/reports.js @@ -20,15 +20,18 @@ router.get('/', (req, res) => { router.get('/allowancebytime/', (req, res) => { - var base_date = validator.isDate(req.param('date')) - ? moment.utc(req.param('date')) + let start_date = validator.isDate(req.param('start_date')) + ? moment.utc(req.param('start_date')) + : req.user.company.get_today(); + + let end_date = validator.isDate(req.param('end_date')) + ? moment.utc(req.param('end_date')) : req.user.company.get_today(); var team_view = new TeamView({ -// base_date : base_date, user : req.user, - start_date : base_date.clone().add(-5, 'months'), - end_date : base_date.clone().add(-1, 'months'), + start_date : start_date, + end_date : end_date, }); var current_deparment_id = validator.isNumeric(req.param('department')) diff --git a/views/audit/emails.hbs b/views/audit/emails.hbs index 06d9c0cde..ee2a3befa 100644 --- a/views/audit/emails.hbs +++ b/views/audit/emails.hbs @@ -26,9 +26,9 @@
    + data-provide="datepicker" data-date-autoclose="1" data-date-format="{{#with logged_user.company }}{{this.get_default_date_format_for_date_picker}}{{/with}}" data-date-week-start="1" + value="{{#if filter.start_date }}{{as_date filter.start_date }}{{/if}}" + >
    diff --git a/views/report/allowancebytime.hbs b/views/report/allowancebytime.hbs index 4f959e5ca..98ba881a2 100644 --- a/views/report/allowancebytime.hbs +++ b/views/report/allowancebytime.hbs @@ -12,49 +12,72 @@
  • Allowance usage by time
  • -
     
    -
    -

    Here will users

    -
    -
    -
    -
    - +
    +
    -
    - + + +
    + +
    + + + + +
    +
    + +
    + +
    + + + + + - +
    +
    +
    +
    + +
    +
    +
    - -
    + + + + + + {{# each users_and_leaves}} - - - + + + {{/each}}
    Employee full nameDays deducted from allowance
    - - {{ statistics.deducted_days }} - -
    {{#with this.user }}{{ this.full_name }}{{/with}}{{ statistics.deducted_days }}
    diff --git a/views/team_view.hbs b/views/team_view.hbs index d9756ac8c..6ab78c6d7 100644 --- a/views/team_view.hbs +++ b/views/team_view.hbs @@ -42,6 +42,7 @@