This commit is contained in:
parent
3e34e7e371
commit
183bf137f6
|
@ -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');
|
||||
|
||||
|
|
|
@ -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));
|
||||
(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);
|
|
@ -282,7 +282,7 @@ ngModule.component('vnCalendar', {
|
|||
bindings: {
|
||||
model: '<',
|
||||
data: '<?',
|
||||
defaultDate: '<?',
|
||||
defaultDate: '=?',
|
||||
onSelection: '&?',
|
||||
onMoveNext: '&?',
|
||||
onMovePrevious: '&?',
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
<vn-empty-rows ng-if="$ctrl.isRefreshing">
|
||||
<vn-spinner enable="$ctrl.isRefreshing"></vn-spinner>
|
||||
</vn-empty-rows>
|
||||
<vn-empty-rows ng-if="!$ctrl.isRefreshing && $ctrl.model && !$ctrl.model.data" translate>
|
||||
Enter a new search
|
||||
</vn-empty-rows>
|
||||
<vn-empty-rows ng-if="!$ctrl.isRefreshing && $ctrl.model.data.length === 0" translate>
|
||||
No results
|
||||
<vn-empty-rows ng-if="!$ctrl.isRefreshing && !$ctrl.model.data">
|
||||
<span translate>No results</span>
|
||||
</vn-empty-rows>
|
||||
</div>
|
|
@ -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'];
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
||||
};
|
|
@ -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];
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -43,5 +43,8 @@
|
|||
},
|
||||
"WorkerCalendar": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"WorkerTimeControl": {
|
||||
"dataSource": "vn"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/worker/filter')(Self);
|
||||
require('../methods/worker/mySubordinates')(Self);
|
||||
require('../methods/worker/isSubordinate')(Self);
|
||||
};
|
||||
|
|
|
@ -10,4 +10,5 @@ import './basic-data';
|
|||
import './pbx';
|
||||
import './department';
|
||||
import './calendar';
|
||||
import './time-control';
|
||||
import './log';
|
||||
|
|
|
@ -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
|
||||
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
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<vn-crud-model
|
||||
vn-id="model"
|
||||
url="/worker/api/WorkerTimeControls/filter"
|
||||
filter="::$ctrl.filter"
|
||||
data="$ctrl.hours">
|
||||
</vn-crud-model>
|
||||
<div class="main-with-right-menu">
|
||||
<vn-card compact pad-large>
|
||||
<vn-horizontal>
|
||||
<vn-table model="model" auto-load="false" vn-one>
|
||||
<vn-thead>
|
||||
<vn-tr>
|
||||
<vn-td ng-repeat="weekday in $ctrl.weekDays" center>
|
||||
<div translate>{{::$ctrl.weekdayNames[$index].name}}</div>
|
||||
{{::weekday.dated | date: 'dd/MM/yyyy'}}
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-thead>
|
||||
<vn-tbody>
|
||||
<vn-tr>
|
||||
<vn-td ng-repeat="weekday in $ctrl.weekDays" class="hours" pad-none center>
|
||||
<vn-label-value ng-repeat="hour in weekday.hours"
|
||||
label="{{($index % 2) == 0 ? 'In' : 'Out'}}"
|
||||
value="{{hour.timed | dateTime: 'HH:mm'}}">
|
||||
</vn-label-value>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-tbody>
|
||||
<vn-tfoot>
|
||||
<vn-tr>
|
||||
<vn-td center ng-repeat="weekday in $ctrl.weekDays">
|
||||
<vn-label-value
|
||||
label="Total"
|
||||
value="{{$ctrl.getWeekdayTotalHours(weekday)}} h.">
|
||||
</vn-label-value>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
<vn-tr>
|
||||
<vn-td center ng-repeat="weekday in $ctrl.weekDays">
|
||||
<vn-icon-button icon="add_circle"
|
||||
vn-tooltip="Add time"
|
||||
ng-click="$ctrl.showAddTimeDialog(weekday)">
|
||||
</vn-icon-button>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-tfoot>
|
||||
</vn-table>
|
||||
</vn-horizontal>
|
||||
</vn-card>
|
||||
<vn-side-menu side="right">
|
||||
<vn-vertical pad-small>
|
||||
<vn-vertical class="totalBox">
|
||||
<h6 translate>Hours</h6>
|
||||
<vn-label-value
|
||||
label="Week total"
|
||||
value="{{$ctrl.weekTotalHours}} {{'Hours' | translate}}">
|
||||
</vn-label-value>
|
||||
</vn-vertical>
|
||||
</vn-vertical>
|
||||
<vn-calendar pad-small
|
||||
data="$ctrl.currentWeek"
|
||||
default-date="$ctrl.defaultDate"
|
||||
on-selection="$ctrl.onSelection(values)"
|
||||
on-move-next="$ctrl.onMoveNext()"
|
||||
on-move-previous="$ctrl.onMovePrevious()">
|
||||
</vn-calendar>
|
||||
</vn-side-menu>
|
||||
</div>
|
||||
|
||||
<vn-dialog vn-id="addTimeDialog"
|
||||
on-response="$ctrl.addTime(response)">
|
||||
<tpl-body>
|
||||
<div>
|
||||
<h5 style="text-align: center">
|
||||
<span translate>Add time</span>
|
||||
</h5>
|
||||
<vn-input-time vn-one model="$ctrl.newTime" label="Hour"></vn-input-time>
|
||||
</div>
|
||||
</tpl-body>
|
||||
<tpl-buttons>
|
||||
<input type="button" response="CANCEL" translate-attr="{value: 'Cancel'}"/>
|
||||
<button response="ACCEPT" translate>Save</button>
|
||||
</tpl-buttons>
|
||||
</vn-dialog>
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue