diff --git a/db/changes/10004-mother/00-acl.sql b/db/changes/10004-mother/00-acl.sql index 78de2dfdb..fca443a69 100644 --- a/db/changes/10004-mother/00-acl.sql +++ b/db/changes/10004-mother/00-acl.sql @@ -1,2 +1,11 @@ UPDATE `salix`.`ACL` SET principalId ='employee' WHERE id = 122; +INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES (167, 'Worker', 'isSubordinate', 'READ', 'ALLOW', 'ROLE', 'employee'); +INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES (168, 'Worker', 'mySubordinates', 'READ', 'ALLOW', 'ROLE', 'employee'); +INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES (169, 'WorkerTimeControl', 'filter', 'READ', 'ALLOW', 'ROLE', 'employee'); +INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES (170, 'WorkerTimeControl', 'addTime', 'WRITE', 'ALLOW', 'ROLE', 'employee'); + diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 728e7c430..68ce7a54c 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -13,7 +13,10 @@ INSERT INTO `vn2008`.`Trabajadores`(`Id_Trabajador`,`CodigoTrabajador`, `Nombre` SELECT id,UPPER(LPAD(role, 3, '0')), name, name, id, 9 FROM `vn`.`user`; -UPDATE `vn2008`.`Trabajadores` SET boss = 1 WHERE Id_Trabajador = 1 OR Id_Trabajador = 9; +UPDATE `vn2008`.`Trabajadores` SET boss = NULL WHERE Id_Trabajador = 20; +UPDATE `vn2008`.`Trabajadores` SET boss = 20 + WHERE Id_Trabajador = 1 OR Id_Trabajador = 9; + DELETE FROM `vn`.`worker` WHERE name ='customer'; INSERT INTO `account`.`user`(`id`,`name`,`password`,`role`,`active`,`email`,`lang`) @@ -1417,4 +1420,11 @@ INSERT INTO `vn`.`sharingClient`(`id`, `workerFk`, `started`, `ended`, `clientFk INSERT INTO `vn`.`sharingCart`(`id`, `workerFk`, `started`, `ended`, `workerSubstitute`, `created`) VALUES - (1, 18, DATE_ADD(CURDATE(), INTERVAL -5 DAY), DATE_ADD(CURDATE(), INTERVAL +15 DAY), 19, DATE_ADD(CURDATE(), INTERVAL -5 DAY)); \ No newline at end of file + (1, 18, DATE_ADD(CURDATE(), INTERVAL -5 DAY), DATE_ADD(CURDATE(), INTERVAL +15 DAY), 19, DATE_ADD(CURDATE(), INTERVAL -5 DAY)); + +INSERT INTO `vn`.`workerTimeControl`(`userFk`,`timed`,`manual`) + VALUES + (106, CONCAT(CURDATE(), ' 07:00'), TRUE), + (106, CONCAT(CURDATE(), ' 10:00'), TRUE), + (106, CONCAT(CURDATE(), ' 10:10'), TRUE), + (106, CONCAT(CURDATE(), ' 15:00'), TRUE); \ No newline at end of file diff --git a/front/core/components/calendar/index.js b/front/core/components/calendar/index.js index bc356053b..46f25bf85 100644 --- a/front/core/components/calendar/index.js +++ b/front/core/components/calendar/index.js @@ -282,7 +282,7 @@ ngModule.component('vnCalendar', { bindings: { model: '<', data: ' - - Enter a new search - - - No results + + No results \ No newline at end of file diff --git a/front/core/module.js b/front/core/module.js index d5fc76578..96685092d 100644 --- a/front/core/module.js +++ b/front/core/module.js @@ -1,6 +1,7 @@ import {ng, ngDeps} from './vendor'; const ngModule = ng.module('vnCore', ngDeps); +ngModule.constant('moment', require('moment-timezone')); export default ngModule; config.$inject = ['$translateProvider', '$translatePartialLoaderProvider']; diff --git a/front/core/vendor.js b/front/core/vendor.js index 7de458072..629c4de4e 100644 --- a/front/core/vendor.js +++ b/front/core/vendor.js @@ -1,5 +1,4 @@ import '@babel/polyfill'; - import * as ng from 'angular'; export {ng}; @@ -10,13 +9,15 @@ import 'mg-crud'; import 'oclazyload'; import 'angular-material'; import 'angular-material/modules/scss/angular-material.scss'; +import 'angular-moment'; export const ngDeps = [ 'pascalprecht.translate', 'ui.router', 'mgCrud', 'oc.lazyLoad', - 'ngMaterial' + 'ngMaterial', + 'angularMoment' ]; import 'material-design-lite'; diff --git a/front/package-lock.json b/front/package-lock.json index a81776d0c..2db5cf9f6 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -46,6 +46,14 @@ "resolved": "https://registry.npmjs.org/angular-material/-/angular-material-1.1.12.tgz", "integrity": "sha512-hvYgVSAxmXy+ozm+FcdGrTrBKm/TLubCgJ8xZR3LNYYmLfsIfzh4Eyk87inmTCXS02KYL0EX2dUeiVmanHlIaQ==" }, + "angular-moment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz", + "integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==", + "requires": { + "moment": ">=2.8.0 <3.0.0" + } + }, "angular-translate": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.1.tgz", @@ -130,6 +138,19 @@ "angular": "^1.6.1" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moment-timezone": { + "version": "0.5.25", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.25.tgz", + "integrity": "sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==", + "requires": { + "moment": ">= 2.9.0" + } + }, "npm": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/npm/-/npm-6.5.0.tgz", diff --git a/front/package.json b/front/package.json index 78ddecce8..3ff4b12e0 100644 --- a/front/package.json +++ b/front/package.json @@ -15,6 +15,7 @@ "angular-animate": "^1.7.7", "angular-aria": "^1.7.7", "angular-material": "^1.1.12", + "angular-moment": "^1.3.0", "angular-translate": "^2.18.1", "angular-translate-loader-partial": "^2.18.1", "flatpickr": "^4.5.2", @@ -22,6 +23,7 @@ "js-yaml": "^3.12.1", "material-design-lite": "^1.3.0", "mg-crud": "^1.1.2", + "moment-timezone": "^0.5.25", "npm": "^6.5.0", "oclazyload": "^0.6.3", "require-yaml": "0.0.1", diff --git a/loopback/locale/es.json b/loopback/locale/es.json index b067b6121..23cb87585 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -82,5 +82,7 @@ "That item is not available on that day": "El item no esta disponible para esa fecha", "That item doesn't exists": "That item doesn't exists", "You cannot add or modify services to an invoiced ticket": "No puedes añadir o modificar servicios a un ticket facturado", - "This ticket can not be modified": "Este ticket no puede ser modificado" + "This ticket can not be modified": "Este ticket no puede ser modificado", + "The introduced hour already exists": "The introduced hour already exists", + "INFINITE_LOOP": "INFINITE_LOOP" } \ No newline at end of file diff --git a/modules/item/back/models/item-tag.js b/modules/item/back/models/item-tag.js index 0e2bfb694..891d11558 100644 --- a/modules/item/back/models/item-tag.js +++ b/modules/item/back/models/item-tag.js @@ -4,14 +4,10 @@ module.exports = Self => { require('../methods/item-tag/filterItemTags')(Self); Self.rewriteDbError(function(err) { + if (err.code === 'ER_DUP_ENTRY') + return new UserError(`The tag can't be repeated`); if (err.code === 'ER_BAD_NULL_ERROR') return new UserError(`Tag value cannot be blank`); return err; }); - - Self.rewriteDbError(function(err) { - if (err.code === 'ER_DUP_ENTRY') - return new UserError(`The tag can't be repeated`); - return err; - }); }; diff --git a/modules/worker/back/methods/worker-calendar/absences.js b/modules/worker/back/methods/worker-calendar/absences.js index b6eae8a13..6319746ab 100644 --- a/modules/worker/back/methods/worker-calendar/absences.js +++ b/modules/worker/back/methods/worker-calendar/absences.js @@ -40,28 +40,13 @@ module.exports = Self => { Self.absences = async(ctx, workerFk, started, ended) => { const models = Self.app.models; - const conn = Self.dataSource.connector; - const myUserId = ctx.req.accessToken.userId; - const myWorker = await models.Worker.findOne({where: {userFk: myUserId}}); - const calendar = {totalHolidays: 0, holidaysEnjoyed: 0}; - const holidays = []; - const stmts = []; + const isSubordinate = await models.Worker.isSubordinate(ctx, workerFk); - // Get subordinates - stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [myWorker.id])); - let subordinatesIndex = stmts.push('SELECT * FROM tmp.subordinate') - 1; - let sql = ParameterizedSQL.join(stmts, ';'); - let result = await conn.executeStmt(sql); - - const subordinates = result[subordinatesIndex]; - const isSubordinate = subordinates.find(subordinate => { - return subordinate.workerFk === workerFk; - }); - const isHr = await models.Account.hasRole(myUserId, 'hr'); - - if (!isHr && workerFk !== myWorker.id && !isSubordinate) + if (!isSubordinate) throw new UserError(`You don't have enough privileges`); + const calendar = {totalHolidays: 0, holidaysEnjoyed: 0}; + const holidays = []; // Get absences of year let absences = await Self.find({ diff --git a/modules/worker/back/methods/worker-time-control/addTime.js b/modules/worker/back/methods/worker-time-control/addTime.js new file mode 100644 index 000000000..cb639f9af --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/addTime.js @@ -0,0 +1,42 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('addTime', { + description: 'Adds a new hour registry', + accessType: 'WRITE', + accepts: [{ + arg: 'data', + type: 'object', + required: true, + description: 'workerFk, timed', + http: {source: 'body'} + }], + returns: [{ + type: 'Object', + root: true + }], + http: { + path: `/addTime`, + verb: 'POST' + } + }); + + Self.addTime = async(ctx, data) => { + const Worker = Self.app.models.Worker; + const myUserId = ctx.req.accessToken.userId; + const myWorker = await Worker.findOne({where: {userFk: myUserId}}); + const isSubordinate = await Worker.isSubordinate(ctx, data.workerFk); + const isTeamBoss = await Self.app.models.Account.hasRole(myUserId, 'teamBoss'); + + if (isSubordinate === false || (isSubordinate && myWorker.id == data.workerFk && !isTeamBoss)) + throw new UserError(`You don't have enough privileges`); + + const subordinate = await Worker.findById(data.workerFk); + + return Self.create({ + userFk: subordinate.userFk, + timed: data.timed, + manual: 1 + }); + }; +}; diff --git a/modules/worker/back/methods/worker-time-control/filter.js b/modules/worker/back/methods/worker-time-control/filter.js new file mode 100644 index 000000000..041c6cbfb --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/filter.js @@ -0,0 +1,46 @@ +const mergeFilters = require('vn-loopback/util/filter').mergeFilters; +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethod('filter', { + description: 'Find all instances of the model matched by filter from the data source.', + accessType: 'READ', + accepts: [{ + arg: 'ctx', + type: 'Object', + http: {source: 'context'} + }, { + arg: 'filter', + type: 'Object', + description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string` + }, { + arg: 'workerFk', + type: 'Number', + required: true, + description: `The worker id` + }], + returns: { + type: ['Object'], + root: true + }, + http: { + path: '/filter', + verb: 'GET' + } + }); + + Self.filter = async(ctx, filter) => { + const Worker = Self.app.models.Worker; + const isSubordinate = await Worker.isSubordinate(ctx, ctx.args.workerFk); + + if (isSubordinate === false) + throw new UserError(`You don't have enough privileges`); + + const subordinate = await Worker.findById(ctx.args.workerFk); + filter = mergeFilters(filter, {where: { + userFk: subordinate.userFk + }}); + + return Self.find(filter); + }; +}; diff --git a/modules/worker/back/methods/worker-time-control/specs/filter.spec.js b/modules/worker/back/methods/worker-time-control/specs/filter.spec.js new file mode 100644 index 000000000..913dfb355 --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/specs/filter.spec.js @@ -0,0 +1,45 @@ +const app = require('vn-loopback/server/server'); + +describe('workerTimeControl filter()', () => { + it('should return 1 result filtering by id', async() => { + let ctx = {req: {accessToken: {userId: 106}}, args: {workerFk: 106}}; + const firstHour = new Date(); + firstHour.setHours(7, 0, 0, 0); + const lastHour = new Date(); + lastHour.setDate(lastHour.getDate() + 1); + lastHour.setHours(15, 0, 0, 0); + + const filter = { + where: { + timed: {between: [firstHour, lastHour]} + } + }; + let result = await app.models.WorkerTimeControl.filter(ctx, filter); + + expect(result.length).toEqual(4); + }); + + it('should return a privilege error for a non subordinate worker', async() => { + let ctx = {req: {accessToken: {userId: 107}}, args: {workerFk: 106}}; + const firstHour = new Date(); + firstHour.setHours(7, 0, 0, 0); + const lastHour = new Date(); + lastHour.setDate(lastHour.getDate() + 1); + lastHour.setHours(15, 0, 0, 0); + + const filter = { + where: { + timed: {between: [firstHour, lastHour]} + } + }; + + let error; + await app.models.WorkerTimeControl.filter(ctx, filter).catch(e => { + error = e; + }).finally(() => { + expect(error.message).toEqual(`You don't have enough privileges`); + }); + + expect(error).toBeDefined(); + }); +}); diff --git a/modules/worker/back/methods/worker/isSubordinate.js b/modules/worker/back/methods/worker/isSubordinate.js new file mode 100644 index 000000000..a85484668 --- /dev/null +++ b/modules/worker/back/methods/worker/isSubordinate.js @@ -0,0 +1,41 @@ +module.exports = Self => { + Self.remoteMethod('isSubordinate', { + description: 'Check if an employee is subordinate', + accessType: 'READ', + accepts: [{ + arg: 'ctx', + type: 'Object', + http: {source: 'context'} + }, { + arg: 'id', + type: 'number', + required: true, + description: 'The worker id', + http: {source: 'path'} + }], + returns: { + type: 'boolean', + root: true + }, + http: { + path: `/:id/isSubordinate`, + verb: 'GET' + } + }); + + Self.isSubordinate = async(ctx, id) => { + const models = Self.app.models; + const myUserId = ctx.req.accessToken.userId; + + const mySubordinates = await Self.mySubordinates(ctx); + const isSubordinate = mySubordinates.find(subordinate => { + return subordinate.workerFk === id; + }); + + const isHr = await models.Account.hasRole(myUserId, 'hr'); + if (isHr || isSubordinate) + return true; + + return false; + }; +}; diff --git a/modules/worker/back/methods/worker/mySubordinates.js b/modules/worker/back/methods/worker/mySubordinates.js new file mode 100644 index 000000000..571edc7f8 --- /dev/null +++ b/modules/worker/back/methods/worker/mySubordinates.js @@ -0,0 +1,39 @@ + +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; + +module.exports = Self => { + Self.remoteMethod('mySubordinates', { + description: 'Returns a list of a subordinate workers', + accessType: 'READ', + accepts: [{ + arg: 'ctx', + type: 'Object', + http: {source: 'context'} + }], + returns: { + type: ['Object'], + root: true + }, + http: { + path: `/mySubordinates`, + verb: 'GET' + } + }); + + Self.mySubordinates = async ctx => { + const conn = Self.dataSource.connector; + const myUserId = ctx.req.accessToken.userId; + const myWorker = await Self.app.models.Worker.findOne({ + where: {userFk: myUserId} + }); + const stmts = []; + + stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [myWorker.id])); + stmts.push('SELECT * FROM tmp.subordinate'); + + let sql = ParameterizedSQL.join(stmts, ';'); + let result = await conn.executeStmt(sql); + + return result[1]; + }; +}; diff --git a/modules/worker/back/methods/worker/specs/isSubordinate.spec.js b/modules/worker/back/methods/worker/specs/isSubordinate.spec.js new file mode 100644 index 000000000..53a620fcc --- /dev/null +++ b/modules/worker/back/methods/worker/specs/isSubordinate.spec.js @@ -0,0 +1,27 @@ +const app = require('vn-loopback/server/server'); + +describe('worker isSubordinate()', () => { + it('should return truthy if a worker is a subordinate', async() => { + let ctx = {req: {accessToken: {userId: 19}}}; + let workerFk = 106; + let result = await app.models.Worker.isSubordinate(ctx, workerFk); + + expect(result).toBeTruthy(); + }); + + it('should return truthy for an hr person', async() => { + let ctx = {req: {accessToken: {userId: 37}}}; + let workerFk = 106; + let result = await app.models.Worker.isSubordinate(ctx, workerFk); + + expect(result).toBeTruthy(); + }); + + it('should return truthy if the current user is himself', async() => { + let ctx = {req: {accessToken: {userId: 106}}}; + let workerFk = 106; + let result = await app.models.Worker.isSubordinate(ctx, workerFk); + + expect(result).toBeTruthy(); + }); +}); diff --git a/modules/worker/back/methods/worker/specs/mySubordinates.spec.js b/modules/worker/back/methods/worker/specs/mySubordinates.spec.js new file mode 100644 index 000000000..5a458a1bc --- /dev/null +++ b/modules/worker/back/methods/worker/specs/mySubordinates.spec.js @@ -0,0 +1,19 @@ +const app = require('vn-loopback/server/server'); + +describe('worker mySubordinates()', () => { + it('should return an array of subordinates greather than 1', async() => { + let ctx = {req: {accessToken: {userId: 9}}}; + let result = await app.models.Worker.mySubordinates(ctx); + + expect(result.length).toBeGreaterThan(1); + }); + + it('should return an array of one subordinate', async() => { + let ctx = {req: {accessToken: {userId: 106}}}; + let result = await app.models.Worker.mySubordinates(ctx); + const workerFk = 106; + + expect(result.length).toEqual(1); + expect(result[0].workerFk).toEqual(workerFk); + }); +}); diff --git a/modules/worker/back/model-config.json b/modules/worker/back/model-config.json index fae2fcc20..3a98fc038 100644 --- a/modules/worker/back/model-config.json +++ b/modules/worker/back/model-config.json @@ -43,5 +43,8 @@ }, "WorkerCalendar": { "dataSource": "vn" + }, + "WorkerTimeControl": { + "dataSource": "vn" } } diff --git a/modules/worker/back/models/worker-time-control.js b/modules/worker/back/models/worker-time-control.js new file mode 100644 index 000000000..4a065f430 --- /dev/null +++ b/modules/worker/back/models/worker-time-control.js @@ -0,0 +1,12 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + require('../methods/worker-time-control/filter')(Self); + require('../methods/worker-time-control/addTime')(Self); + + Self.rewriteDbError(function(err) { + if (err.code === 'ER_DUP_ENTRY') + return new UserError(`The introduced hour already exists`); + return err; + }); +}; diff --git a/modules/worker/back/models/worker-time-control.json b/modules/worker/back/models/worker-time-control.json new file mode 100644 index 000000000..bfd6a44aa --- /dev/null +++ b/modules/worker/back/models/worker-time-control.json @@ -0,0 +1,36 @@ +{ + "name": "WorkerTimeControl", + "base": "VnModel", + "options": { + "mysql": { + "table": "workerTimeControl" + } + }, + "properties": { + "id": { + "id": true, + "type": "Number" + }, + "timed": { + "type": "Date" + }, + "manual": { + "type": "Boolean" + }, + "order": { + "type": "Number" + } + }, + "relations": { + "user": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "userFk" + }, + "warehouse": { + "type": "belongsTo", + "model": "Warehouse", + "foreignKey": "warehouseFk" + } + } +} diff --git a/modules/worker/back/models/worker.js b/modules/worker/back/models/worker.js index 91939c66f..ba4a396ee 100644 --- a/modules/worker/back/models/worker.js +++ b/modules/worker/back/models/worker.js @@ -1,3 +1,5 @@ module.exports = Self => { require('../methods/worker/filter')(Self); + require('../methods/worker/mySubordinates')(Self); + require('../methods/worker/isSubordinate')(Self); }; diff --git a/modules/worker/front/index.js b/modules/worker/front/index.js index dce6d394a..71c529ea8 100644 --- a/modules/worker/front/index.js +++ b/modules/worker/front/index.js @@ -10,4 +10,5 @@ import './basic-data'; import './pbx'; import './department'; import './calendar'; +import './time-control'; import './log'; diff --git a/modules/worker/front/locale/es.yml b/modules/worker/front/locale/es.yml index d13c1d831..fb8108e4e 100644 --- a/modules/worker/front/locale/es.yml +++ b/modules/worker/front/locale/es.yml @@ -15,4 +15,5 @@ Fiscal Identifier: NIF User name: Usuario Departments: Departamentos Calendar: Calendario -Search workers by id, firstName, lastName or user name: Buscar trabajadores por el identificador, nombre, apellidos o nombre de usuario \ No newline at end of file +Search workers by id, firstName, lastName or user name: Buscar trabajadores por el identificador, nombre, apellidos o nombre de usuario +Time control: Control de horario \ No newline at end of file diff --git a/modules/worker/front/routes.json b/modules/worker/front/routes.json index 8b2fe193d..1b4f9ec2a 100644 --- a/modules/worker/front/routes.json +++ b/modules/worker/front/routes.json @@ -6,7 +6,8 @@ "menu": [ {"state": "worker.card.basicData", "icon": "settings"}, {"state": "worker.card.pbx", "icon": "icon-pbx"}, - {"state": "worker.card.calendar", "icon": "icon-calendar"} + {"state": "worker.card.calendar", "icon": "icon-calendar"}, + {"state": "worker.card.timeControl", "icon": "access_time"} ], "routes": [ { @@ -64,6 +65,15 @@ "worker": "$ctrl.worker" } }, + { + "url": "/time-control", + "state": "worker.card.timeControl", + "component": "vn-worker-time-control", + "description": "Time control", + "params": { + "worker": "$ctrl.worker" + } + }, { "url" : "/department", "state": "worker.department", diff --git a/modules/worker/front/time-control/index.html b/modules/worker/front/time-control/index.html new file mode 100644 index 000000000..f471a7d0b --- /dev/null +++ b/modules/worker/front/time-control/index.html @@ -0,0 +1,84 @@ + + +
+ + + + + + +
{{::$ctrl.weekdayNames[$index].name}}
+ {{::weekday.dated | date: 'dd/MM/yyyy'}} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + +
Hours
+ + +
+
+ + +
+
+ + + +
+
+ Add time +
+ +
+
+ + + + +
\ No newline at end of file diff --git a/modules/worker/front/time-control/index.js b/modules/worker/front/time-control/index.js new file mode 100644 index 000000000..47eacc8e2 --- /dev/null +++ b/modules/worker/front/time-control/index.js @@ -0,0 +1,241 @@ +import ngModule from '../module'; +import './style.scss'; + +class Controller { + constructor($scope, $http, $stateParams) { + this.$stateParams = $stateParams; + this.$ = $scope; + this.$http = $http; + this.defaultDate = new Date(); + this.currentWeek = []; + this.weekDays = []; + this.weekdayNames = [ + {name: 'Monday'}, + {name: 'Tuesday'}, + {name: 'Wednesday'}, + {name: 'Thursday'}, + {name: 'Friday'}, + {name: 'Saturday'}, + {name: 'Sunday'} + ]; + } + + + /** + * Gets worker data + */ + get worker() { + return this._worker; + } + + /** + * Sets worker data and retrieves + * worker hours + * + * @param {Object} value - Worker data + */ + set worker(value) { + this._worker = value; + + if (value) { + this.$.$applyAsync(() => { + this.getHours(); + }); + } + } + + /** + * Gets worker hours data + * + */ + get hours() { + return this._hours; + } + + /** + * Gets worker hours data + * + * @param {Object} value - Hours data + */ + set hours(value) { + this._hours = value; + if (value) this.build(); + } + + getHours() { + const params = {workerFk: this.worker.id}; + const filter = { + where: { + timed: {between: [this.started, this.ended]} + } + }; + + return this.$.model.applyFilter(filter, params); + } + + refresh() { + this.getHours().then(() => this.build()); + } + + build() { + let weekdays = new Array(7); + const currentWeek = []; + + for (let i = 0; i < weekdays.length; i++) { + const dated = new Date(); + dated.setHours(0, 0, 0, 0); + dated.setMonth(this.started.getMonth()); + dated.setDate(this.started.getDate() + i); + + const hours = this.hours.filter(hour => { + const timed = new Date(hour.timed); + const weekDay = timed.getDay() == 0 ? 7 : timed.getDay(); + return weekDay == (i + 1); + }); + + const sortedHours = hours.sort((a, b) => { + return new Date(a.timed) - new Date(b.timed); + }); + + weekdays[i] = {dated, hours: sortedHours}; + + currentWeek.push({ + dated: dated, + className: 'orange-circle', + title: 'Current week', + isRemovable: false + }); + } + + this.currentWeek = currentWeek; + this.weekDays = weekdays; + } + + get weekOffset() { + const currentDate = this.defaultDate; + const weekDay = currentDate.getDay() + 1; + + return weekDay - 2; + } + + /** + * Returns week start date + * from current selected week + */ + get started() { + const started = new Date(); + started.setMonth(this.defaultDate.getMonth()); + started.setDate(this.defaultDate.getDate() - this.weekOffset); + started.setHours(0, 0, 0, 0); + + return started; + } + + /** + * Returns week end date + * from current selected week + */ + get ended() { + const ended = new Date(); + ended.setHours(0, 0, 0, 0); + ended.setMonth(this.started.getMonth()); + ended.setDate(this.started.getDate() + 7); + + return ended; + } + + getWeekdayTotalHours(weekday) { + if (weekday.hours.length == 0) return 0; + + const hours = weekday.hours; + + let totalStamp = 0; + + hours.forEach((hour, index) => { + let currentHour = new Date(hour.timed); + let previousHour = new Date(hour.timed); + + if (index > 0 && (index % 2 == 1)) + previousHour = new Date(hours[index - 1].timed); + + const dif = Math.abs(previousHour - currentHour); + + totalStamp += dif; + }); + + if (totalStamp / 3600 / 1000 > 5) + totalStamp += (20 * 60 * 1000); + + weekday.total = totalStamp; + + return this.formatHours(totalStamp); + } + + get weekTotalHours() { + let total = 0; + + this.weekDays.forEach(weekday => { + if (weekday.total) + total += weekday.total; + }); + return this.formatHours(total); + } + + formatHours(timestamp) { + let hour = Math.floor(timestamp / 3600 / 1000); + let min = Math.floor(timestamp / 60 / 1000 - 60 * hour); + + if (hour < 10) hour = `0${hour}`; + if (min < 10) min = `0${min}`; + + return `${hour}:${min}`; + } + + onMoveNext() { + this.refresh(); + } + + onMovePrevious() { + this.refresh(); + } + + onSelection(value) { + const selected = value[0].dated; + this.defaultDate.setMonth(selected.getMonth()); + this.defaultDate.setDate(selected.getDate() - 1); + this.refresh(); + } + + showAddTimeDialog(weekday) { + const timed = new Date(weekday.dated); + const now = new Date(); + now.setHours(now.getHours(), now.getMinutes(), 0, 0); + now.setMonth(timed.getMonth()); + now.setDate(timed.getDate()); + + + this.newTime = now; + this.selectedWeekday = weekday; + this.$.addTimeDialog.show(); + } + + addTime(response) { + if (response === 'ACCEPT') { + let data = {workerFk: this.worker.id, timed: this.newTime}; + let query = `/api/WorkerTimeControls/addTime`; + this.$http.post(query, data).then(() => { + this.refresh(); + }); + } + } +} + +Controller.$inject = ['$scope', '$http', '$stateParams']; + +ngModule.component('vnWorkerTimeControl', { + template: require('./index.html'), + controller: Controller, + bindings: { + worker: '<' + } +}); diff --git a/modules/worker/front/time-control/index.spec.js b/modules/worker/front/time-control/index.spec.js new file mode 100644 index 000000000..a9114a193 --- /dev/null +++ b/modules/worker/front/time-control/index.spec.js @@ -0,0 +1,94 @@ +import './index.js'; + +describe('Worker', () => { + describe('Component vnWorkerTimeControl', () => { + let $scope; + let controller; + + beforeEach(ngModule('worker')); + + beforeEach(angular.mock.inject(($componentController, $rootScope) => { + $scope = $rootScope.$new(); + controller = $componentController('vnWorkerTimeControl', {$scope}); + })); + + describe('worker() setter', () => { + it(`should set worker data and then call getHours() method`, () => { + spyOn(controller, 'getHours'); + controller.worker = {id: 106}; + $scope.$apply(); + + expect(controller.getHours).toHaveBeenCalledWith(); + }); + }); + + describe('hours() setter', () => { + it(`should set hours data and then call build() method`, () => { + spyOn(controller, 'build'); + controller.hours = [{id: 1}, {id: 2}]; + $scope.$apply(); + + expect(controller.build).toHaveBeenCalledWith(); + }); + }); + + describe('getWeekdayTotalHours() ', () => { + it(`should return a total worked hours from 07:00 to 15:00`, () => { + const hourOne = new Date(); + hourOne.setHours(7, 0, 0, 0); + const hourTwo = new Date(); + hourTwo.setHours(10, 0, 0, 0); + const hourThree = new Date(); + hourThree.setHours(10, 20, 0, 0); + const hourFour = new Date(); + hourFour.setHours(15, 0, 0, 0); + + const weekday = {hours: [ + {id: 1, timed: hourOne}, + {id: 2, timed: hourTwo}, + {id: 3, timed: hourThree}, + {id: 4, timed: hourFour} + ]}; + + const result = controller.getWeekdayTotalHours(weekday); + + expect(result).toEqual('08:00'); + }); + }); + + describe('weekTotalHours() ', () => { + it(`should return a total worked hours from a week`, () => { + const hourOne = new Date(); + hourOne.setHours(7, 0, 0, 0); + const hourTwo = new Date(); + hourTwo.setHours(10, 0, 0, 0); + const hourThree = new Date(); + hourThree.setHours(10, 20, 0, 0); + const hourFour = new Date(); + hourFour.setHours(15, 0, 0, 0); + + const weekday = {hours: [ + {id: 1, timed: hourOne}, + {id: 2, timed: hourTwo}, + {id: 3, timed: hourThree}, + {id: 4, timed: hourFour} + ]}; + controller.weekDays = [weekday]; + + const weekdayHours = controller.getWeekdayTotalHours(weekday); + const weekHours = controller.weekTotalHours; + + expect(weekdayHours).toEqual('08:00'); + expect(weekHours).toEqual('08:00'); + }); + }); + + describe('formatHours() ', () => { + it(`should format a passed timestamp to hours and minutes`, () => { + const result = controller.formatHours(3600000); + + expect(result).toEqual('01:00'); + }); + }); + }); +}); diff --git a/modules/worker/front/time-control/locale/es.yml b/modules/worker/front/time-control/locale/es.yml new file mode 100644 index 000000000..71436d507 --- /dev/null +++ b/modules/worker/front/time-control/locale/es.yml @@ -0,0 +1,14 @@ +In: Entrada +Out: Salida +Monday: Lunes +Tuesday: Martes +Wednesday: Miércoles +Thursday: Jueves +Friday: Viernes +Saturday: Sábado +Sunday: Domingo +Hour: Hora +Hours: Horas +Add time: Añadir hora +Week total: Total por semana +Current week: Semana actual \ No newline at end of file diff --git a/modules/worker/front/time-control/style.scss b/modules/worker/front/time-control/style.scss new file mode 100644 index 000000000..7979394b6 --- /dev/null +++ b/modules/worker/front/time-control/style.scss @@ -0,0 +1,20 @@ +@import "variables"; + +vn-worker-time-control { + vn-thead > vn-tr > vn-td > div { + margin-bottom: 5px; + color: $color-main + } + + vn-td.hours { + vertical-align: top; + + vn-label-value { + padding: .6em .5em + } + } + + .totalBox { + max-width: none + } +} \ No newline at end of file