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 }