added workerTimeControl #1360
gitea/salix/dev This commit has test failures Details

This commit is contained in:
Joan Sanchez 2019-05-17 13:27:51 +02:00
parent 3e34e7e371
commit 183bf137f6
30 changed files with 839 additions and 38 deletions

View File

@ -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');

View File

@ -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);

View File

@ -282,7 +282,7 @@ ngModule.component('vnCalendar', {
bindings: {
model: '<',
data: '<?',
defaultDate: '<?',
defaultDate: '=?',
onSelection: '&?',
onMoveNext: '&?',
onMovePrevious: '&?',

View File

@ -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>

View File

@ -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'];

View File

@ -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';

View File

@ -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",

View File

@ -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",

View File

@ -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"
}

View File

@ -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;
});
};

View File

@ -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({

View File

@ -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
});
};
};

View File

@ -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);
};
};

View File

@ -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();
});
});

View File

@ -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;
};
};

View File

@ -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];
};
};

View File

@ -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();
});
});

View File

@ -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);
});
});

View File

@ -43,5 +43,8 @@
},
"WorkerCalendar": {
"dataSource": "vn"
},
"WorkerTimeControl": {
"dataSource": "vn"
}
}

View File

@ -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;
});
};

View File

@ -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"
}
}
}

View File

@ -1,3 +1,5 @@
module.exports = Self => {
require('../methods/worker/filter')(Self);
require('../methods/worker/mySubordinates')(Self);
require('../methods/worker/isSubordinate')(Self);
};

View File

@ -10,4 +10,5 @@ import './basic-data';
import './pbx';
import './department';
import './calendar';
import './time-control';
import './log';

View File

@ -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

View File

@ -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",

View File

@ -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>

View File

@ -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: '<'
}
});

View File

@ -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');
});
});
});
});

View File

@ -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

View File

@ -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
}
}