diff --git a/front/core/components/chip/style.scss b/front/core/components/chip/style.scss
index ee7f46848..0d19fc957 100644
--- a/front/core/components/chip/style.scss
+++ b/front/core/components/chip/style.scss
@@ -1,7 +1,7 @@
@import "variables";
vn-chip {
- border-radius: 16px;
+ border-radius: 1em;
background-color: $color-bg;
margin: 0 0.5em 0.5em 0;
color: $color-font;
@@ -11,7 +11,7 @@ vn-chip {
align-items: center;
text-overflow: ellipsis;
white-space: nowrap;
- height: 28px;
+ height: 2em;
padding: 0 .7em;
overflow: hidden;
@@ -47,7 +47,7 @@ vn-chip {
vn-avatar {
display: inline-block;
- height: 28px;
- width: 28px;
+ height: 2em;
+ width: 2em;
border-radius: 50%;
}
\ No newline at end of file
diff --git a/modules/worker/back/methods/worker-time-control/addTime.js b/modules/worker/back/methods/worker-time-control/addTimeEntry.js
similarity index 91%
rename from modules/worker/back/methods/worker-time-control/addTime.js
rename to modules/worker/back/methods/worker-time-control/addTimeEntry.js
index 8130a16fd..649364151 100644
--- a/modules/worker/back/methods/worker-time-control/addTime.js
+++ b/modules/worker/back/methods/worker-time-control/addTimeEntry.js
@@ -1,7 +1,7 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
- Self.remoteMethodCtx('addTime', {
+ Self.remoteMethodCtx('addTimeEntry', {
description: 'Adds a new hour registry',
accessType: 'WRITE',
accepts: [{
@@ -16,12 +16,12 @@ module.exports = Self => {
root: true
}],
http: {
- path: `/addTime`,
+ path: `/addTimeEntry`,
verb: 'POST'
}
});
- Self.addTime = async(ctx, data) => {
+ Self.addTimeEntry = async(ctx, data) => {
const Worker = Self.app.models.Worker;
const myUserId = ctx.req.accessToken.userId;
const myWorker = await Worker.findOne({where: {userFk: myUserId}});
diff --git a/modules/worker/back/methods/worker-time-control/deleteTimeEntry.js b/modules/worker/back/methods/worker-time-control/deleteTimeEntry.js
new file mode 100644
index 000000000..717d4ce8c
--- /dev/null
+++ b/modules/worker/back/methods/worker-time-control/deleteTimeEntry.js
@@ -0,0 +1,35 @@
+module.exports = Self => {
+ Self.remoteMethodCtx('deleteTimeEntry', {
+ description: 'Deletes a manual time entry for a worker if the user role is above the worker',
+ accessType: 'READ',
+ accepts: [{
+ arg: 'id',
+ type: 'number',
+ required: true,
+ description: 'The time entry id',
+ http: {source: 'path'}
+ }],
+ returns: {
+ type: 'boolean',
+ root: true
+ },
+ http: {
+ path: `/:id/deleteTimeEntry`,
+ verb: 'POST'
+ }
+ });
+
+ Self.deleteTimeEntry = async(ctx, id) => {
+ const workerModel = Self.app.models.Worker;
+
+ const targetTimeEntry = await Self.findById(id);
+
+ const hasRightsToDelete = await workerModel.isSubordinate(ctx, targetTimeEntry.userFk);
+
+ if (!hasRightsToDelete)
+ throw new UserError(`You don't have enough privileges`);
+
+ return Self.rawSql('CALL vn.workerTimeControl_remove(?, ?)', [
+ targetTimeEntry.userFk, targetTimeEntry.timed]);
+ };
+};
diff --git a/modules/worker/back/methods/worker/getWorkedHours.js b/modules/worker/back/methods/worker/getWorkedHours.js
new file mode 100644
index 000000000..e96d30f8d
--- /dev/null
+++ b/modules/worker/back/methods/worker/getWorkedHours.js
@@ -0,0 +1,67 @@
+const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
+
+module.exports = Self => {
+ Self.remoteMethod('getWorkedHours', {
+ description: 'returns the total worked hours per day for a given range of dates in format YYYY-mm-dd hh:mm:ss',
+ accessType: 'READ',
+ accepts: [{
+ arg: 'id',
+ type: 'number',
+ required: true,
+ description: 'The worker id',
+ http: {source: 'path'}
+ },
+ {
+ arg: 'from',
+ type: 'Date',
+ required: true,
+ description: `The from date`
+ },
+ {
+ arg: 'to',
+ type: 'Date',
+ required: true,
+ description: `The to date`
+ }],
+ returns: {
+ type: ['Object'],
+ root: true
+ },
+ http: {
+ path: `/:id/getWorkedHours`,
+ verb: 'GET'
+ }
+ });
+
+ Self.getWorkedHours = async(id, from, to) => {
+ const conn = Self.dataSource.connector;
+ const stmts = [];
+
+ let worker = await Self.app.models.Worker.findById(id);
+ let userId = worker.userFk;
+
+ stmts.push(`
+ DROP TEMPORARY TABLE IF EXISTS
+ tmp.timeControlCalculate,
+ tmp.timeBusinessCalculate
+ `);
+
+ stmts.push(new ParameterizedSQL('CALL vn.timeControl_calculateByUser(?, ?, ?)', [userId, from, to]));
+ stmts.push(new ParameterizedSQL('CALL vn.timeBusiness_calculateByUser(?, ?, ?)', [userId, from, to]));
+ let resultIndex = stmts.push(`
+ SELECT tbc.dated, tbc.timeWorkSeconds expectedHours, tcc.timeWorkSeconds workedHours
+ FROM tmp.timeBusinessCalculate tbc
+ LEFT JOIN tmp.timeControlCalculate tcc ON tcc.dated = tbc.dated
+ `) - 1;
+ stmts.push(`
+ DROP TEMPORARY TABLE IF EXISTS
+ tmp.timeControlCalculate,
+ tmp.timeBusinessCalculate
+ `);
+
+ let sql = ParameterizedSQL.join(stmts, ';');
+ let result = await conn.executeStmt(sql);
+
+ return result[resultIndex];
+ };
+};
diff --git a/modules/worker/back/methods/worker/timeControl.js b/modules/worker/back/methods/worker/timeControl.js
new file mode 100644
index 000000000..bc88197fe
--- /dev/null
+++ b/modules/worker/back/methods/worker/timeControl.js
@@ -0,0 +1,50 @@
+
+const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
+
+module.exports = Self => {
+ Self.remoteMethod('timeControl', {
+ description: 'Returns a range of worked hours for a given worker id',
+ accessType: 'READ',
+ accepts: [
+ {
+ arg: 'id',
+ type: 'number',
+ required: true,
+ description: 'The worker id',
+ http: {source: 'path'}
+ },
+ {
+ arg: 'dateFrom',
+ type: 'datetime',
+ required: true,
+ description: 'the date from the time control begins in format YYYY-mm-dd-hh:mm:ss'
+ },
+ {
+ arg: 'dateTo',
+ type: 'datetime',
+ required: true,
+ description: 'the date when the time control finishes in format YYYY-mm-dd-hh:mm:ss'
+ }],
+ returns: {
+ type: ['Object'],
+ root: true
+ },
+ http: {
+ path: `/:id/timeControl`,
+ verb: 'GET'
+ }
+ });
+
+ Self.timeControl = async(id, from, to) => {
+ const conn = Self.dataSource.connector;
+ const stmts = [];
+
+ stmts.push(new ParameterizedSQL('CALL vn.timeControl_calculateByUser(?, ?, ?)', [id, from, to]));
+
+ let sql = ParameterizedSQL.join(stmts, ';');
+ let result = await conn.executeStmt(sql);
+
+
+ return result[0];
+ };
+};
diff --git a/modules/worker/back/models/worker-time-control.js b/modules/worker/back/models/worker-time-control.js
index 4a065f430..f0191c36f 100644
--- a/modules/worker/back/models/worker-time-control.js
+++ b/modules/worker/back/models/worker-time-control.js
@@ -2,7 +2,8 @@ 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);
+ require('../methods/worker-time-control/addTimeEntry')(Self);
+ require('../methods/worker-time-control/deleteTimeEntry')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
diff --git a/modules/worker/back/models/worker-time-control.json b/modules/worker/back/models/worker-time-control.json
index bfd6a44aa..5212222bd 100644
--- a/modules/worker/back/models/worker-time-control.json
+++ b/modules/worker/back/models/worker-time-control.json
@@ -19,6 +19,9 @@
},
"order": {
"type": "Number"
+ },
+ "direction": {
+ "type": "string"
}
},
"relations": {
@@ -26,6 +29,11 @@
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
+ },
+ "worker": {
+ "type": "hasOne",
+ "model": "Worker",
+ "foreignKey": "userFk"
},
"warehouse": {
"type": "belongsTo",
diff --git a/modules/worker/back/models/worker.js b/modules/worker/back/models/worker.js
index 1d2a62ce4..e49243d0f 100644
--- a/modules/worker/back/models/worker.js
+++ b/modules/worker/back/models/worker.js
@@ -3,4 +3,5 @@ module.exports = Self => {
require('../methods/worker/mySubordinates')(Self);
require('../methods/worker/isSubordinate')(Self);
require('../methods/worker/getWorkerInfo')(Self);
+ require('../methods/worker/getWorkedHours')(Self);
};
diff --git a/modules/worker/front/time-control/index.html b/modules/worker/front/time-control/index.html
index 54c44973c..bd22a6be2 100644
--- a/modules/worker/front/time-control/index.html
+++ b/modules/worker/front/time-control/index.html
@@ -5,28 +5,55 @@
data="$ctrl.hours">
-
+
-
- {{::$ctrl.weekdayNames[$index].name}}
- {{::weekday.dated | date: 'dd'}}
-
- {{::weekday.dated | date: 'MMMM'}}
-
+
+ {{::$ctrl.weekdayNames[$index].name}}
+
+ {{::weekday.dated | date: 'dd'}}
+
+ {{::weekday.dated | date: 'MMMM'}}
+
+
+
+
+
+
+ {{::weekday.event.name}}
+
+
-
+
+ icon="{{
+ ::hour.direction == 'in' ? 'arrow_forward'
+ : hour.direction == 'out' ? 'arrow_back'
+ : 'arrow_forward'
+ }}"
+ title="{{
+ ::(hour.direction == 'in' ? 'In'
+ : hour.direction == 'out' ? 'Out'
+ : '') | translate
+ }}"
+ ng-class="::{'invisible': hour.direction == 'middle'}">
- {{hour.timed | date: 'HH:mm'}}
+
+ {{::hour.timed | date: 'HH:mm'}}
+
@@ -34,7 +61,7 @@
- {{$ctrl.getWeekdayTotalHours(weekday)}} h.
+ {{$ctrl.formatHours(weekday.workedHours)}} h.
@@ -51,6 +78,12 @@
+
+
+
+
Hours
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/modules/worker/front/time-control/index.js b/modules/worker/front/time-control/index.js
index 622ecd16e..508669bbc 100644
--- a/modules/worker/front/time-control/index.js
+++ b/modules/worker/front/time-control/index.js
@@ -2,13 +2,15 @@ import ngModule from '../module';
import './style.scss';
class Controller {
- constructor($scope, $http, $stateParams, $element, vnWeekDays) {
+ constructor($scope, $http, $stateParams, $element, vnWeekDays, vnApp, $translate) {
this.$stateParams = $stateParams;
this.$ = $scope;
this.$http = $http;
this.$element = $element;
this.weekDays = [];
this.weekdayNames = vnWeekDays.locales;
+ this.vnApp = vnApp;
+ this.$translate = $translate;
}
$postLink() {
@@ -34,9 +36,20 @@ class Controller {
this.started = started;
let ended = new Date(started.getTime());
- ended.setDate(ended.getDate() + 7);
+ ended.setHours(23, 59, 59, 59);
+ ended.setDate(ended.getDate() + 6);
this.ended = ended;
+ this.weekDays = [];
+ let dayIndex = new Date(started.getTime());
+
+ while (dayIndex < ended) {
+ this.weekDays.push({
+ dated: new Date(dayIndex.getTime())
+ });
+ dayIndex.setDate(dayIndex.getDate() + 1);
+ }
+
this.fetchHours();
}
@@ -49,23 +62,15 @@ class Controller {
set hours(value) {
this._hours = value;
- this.weekDays = [];
- if (!this.hours) return;
- let dayIndex = new Date(this.started.getTime());
-
- while (dayIndex < this.ended) {
- let weekDay = dayIndex.getDay();
-
- let hours = this.hours
- .filter(hour => new Date(hour.timed).getDay() == weekDay)
- .sort((a, b) => new Date(a.timed) - new Date(b.timed));
-
- this.weekDays.push({
- dated: new Date(dayIndex.getTime()),
- hours
- });
- dayIndex.setDate(dayIndex.getDate() + 1);
+ for (const weekDay of this.weekDays) {
+ if (value) {
+ let day = weekDay.dated.getDay();
+ weekDay.hours = value
+ .filter(hour => new Date(hour.timed).getDay() == day)
+ .sort((a, b) => new Date(a.timed) - new Date(b.timed));
+ } else
+ weekDay.hours = null;
}
}
@@ -74,61 +79,89 @@ class Controller {
const filter = {
where: {and: [
{timed: {gte: this.started}},
- {timed: {lt: this.ended}}
+ {timed: {lte: this.ended}}
]}
};
-
this.$.model.applyFilter(filter, params);
+
+ this.getAbsences();
+ this.getWorkedHours(this.started, this.ended);
}
hasEvents(day) {
return day >= this.started && day < this.ended;
}
- hourColor(weekDay) {
- return weekDay.manual ? 'alert' : 'warning';
+ getWorkedHours(from, to) {
+ let params = {
+ id: this.$stateParams.id,
+ from: from,
+ to: to
+ };
+
+ const query = `api/workers/${this.$stateParams.id}/getWorkedHours`;
+ return this.$http.get(query, {params}).then(res => {
+ const workDays = res.data;
+ const map = new Map();
+
+ for (const workDay of workDays) {
+ workDay.dated = new Date(workDay.dated);
+ map.set(workDay.dated, workDay);
+ }
+
+ for (const weekDay of this.weekDays) {
+ const workDay = workDays.find(day => {
+ let from = new Date(day.dated);
+ from.setHours(0, 0, 0, 0);
+
+ let to = new Date(day.dated);
+ to.setHours(23, 59, 59, 59);
+
+ return weekDay.dated >= from && weekDay.dated <= to;
+ });
+ weekDay.expectedHours = workDay.expectedHours;
+ weekDay.workedHours = workDay.workedHours;
+ }
+ });
}
- getWeekdayTotalHours(weekday) {
- if (weekday.hours.length == 0) return 0;
+ getFinishTime() {
+ let weekOffset = new Date().getDay() - 1;
+ if (weekOffset < 0) weekOffset = 6;
+ const today = this.weekDays[weekOffset];
- const hours = weekday.hours;
+ if (today && today.workedHours) {
+ const remainingTime = (today.expectedHours - today.workedHours) * 1000;
+ const lastKnownTime = new Date(today.hours[today.hours.length - 1].timed).getTime();
+ const finishTimeStamp = lastKnownTime + remainingTime;
- let totalStamp = 0;
+ let finishDate = new Date(finishTimeStamp);
+ let hour = finishDate.getHours();
+ let minute = finishDate.getMinutes();
- hours.forEach((hour, index) => {
- let currentHour = new Date(hour.timed);
- let previousHour = new Date(hour.timed);
+ if (hour < 10) hour = `0${hour}`;
+ if (minute < 10) minute = `0${minute}`;
- 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);
+ return `${hour}:${minute} h.`;
+ }
}
get weekTotalHours() {
let total = 0;
this.weekDays.forEach(weekday => {
- if (weekday.total)
- total += weekday.total;
+ if (weekday.workedHours)
+ total += weekday.workedHours;
});
+
return this.formatHours(total);
}
formatHours(timestamp) {
- let hour = Math.floor(timestamp / 3600 / 1000);
- let min = Math.floor(timestamp / 60 / 1000 - 60 * hour);
+ timestamp = timestamp || 0;
+
+ let hour = Math.floor(timestamp / 3600);
+ let min = Math.floor(timestamp / 60 - 60 * hour);
if (hour < 10) hour = `0${hour}`;
if (min < 10) min = `0${min}`;
@@ -159,12 +192,75 @@ class Controller {
workerFk: this.$stateParams.id,
timed: this.newTime
};
- this.$http.post(`api/WorkerTimeControls/addTime`, data)
+ this.$http.post(`api/WorkerTimeControls/addTimeEntry`, data)
.then(() => this.fetchHours());
}
+
+ showDeleteDialog(hour) {
+ this.timeEntryToDelete = hour;
+ this.$.deleteEntryDialog.show();
+ }
+
+ deleteTimeEntry(response) {
+ if (response !== 'ACCEPT') return;
+
+ const entryId = this.timeEntryToDelete.id;
+
+ this.$http.post(`api/WorkerTimeControls/${entryId}/deleteTimeEntry`).then(() => {
+ this.fetchHours();
+ this.vnApp.showSuccess(this.$translate.instant('Entry removed'));
+ });
+ }
+
+ getAbsences() {
+ let params = {
+ workerFk: this.$stateParams.id,
+ started: this.started,
+ ended: this.ended
+ };
+
+ return this.$http.get(`api/WorkerCalendars/absences`, {params})
+ .then(res => this.onData(res.data));
+ }
+
+ onData(data) {
+ const events = {};
+
+ let addEvent = (day, event) => {
+ events[new Date(day).getTime()] = event;
+ };
+
+ if (data.holidays) {
+ data.holidays.forEach(holiday => {
+ const holidayDetail = holiday.detail && holiday.detail.description;
+ const holidayType = holiday.type && holiday.type.name;
+ const holidayName = holidayDetail || holidayType;
+
+ addEvent(holiday.dated, {
+ name: holidayName,
+ color: '#ff0'
+ });
+ });
+ }
+ if (data.absences) {
+ data.absences.forEach(absence => {
+ const type = absence.absenceType;
+ addEvent(absence.dated, {
+ name: type.name,
+ color: type.rgb
+ });
+ });
+ }
+
+ this.weekDays.forEach(day => {
+ const timestamp = day.dated.getTime();
+ if (events[timestamp])
+ day.event = events[timestamp];
+ });
+ }
}
-Controller.$inject = ['$scope', '$http', '$stateParams', '$element', 'vnWeekDays'];
+Controller.$inject = ['$scope', '$http', '$stateParams', '$element', 'vnWeekDays', 'vnApp', '$translate'];
ngModule.component('vnWorkerTimeControl', {
template: require('./index.html'),
diff --git a/modules/worker/front/time-control/locale/es.yml b/modules/worker/front/time-control/locale/es.yml
index 9a3484fc6..8ddab11a1 100644
--- a/modules/worker/front/time-control/locale/es.yml
+++ b/modules/worker/front/time-control/locale/es.yml
@@ -4,4 +4,7 @@ Hour: Hora
Hours: Horas
Add time: Añadir hora
Week total: Total semana
-Current week: Semana actual
\ No newline at end of file
+Current week: Semana actual
+This time entry will be deleted: Se borrará la hora fichada
+Are you sure you want to delete this entry?: ¿Seguro que quieres eliminarla?
+Finish at: Termina a las
\ No newline at end of file
diff --git a/modules/worker/front/time-control/style.scss b/modules/worker/front/time-control/style.scss
index 978a600a2..ba0ea4d64 100644
--- a/modules/worker/front/time-control/style.scss
+++ b/modules/worker/front/time-control/style.scss
@@ -1,7 +1,7 @@
@import "variables";
vn-worker-time-control {
- vn-thead > vn-tr > vn-td > div {
+ vn-thead > vn-tr > vn-td > div.weekday {
margin-bottom: 5px;
color: $color-main
}