diff --git a/db/changes/10502-november/00-workerTimeControlMail.sql b/db/changes/10502-november/00-workerTimeControlMail.sql new file mode 100644 index 000000000..e3d169a83 --- /dev/null +++ b/db/changes/10502-november/00-workerTimeControlMail.sql @@ -0,0 +1 @@ +ALTER TABLE `vn`.`workerTimeControlMail` CHANGE emailResponse reason text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL NULL; \ No newline at end of file diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 7f676a18f..6624e99f4 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2267,12 +2267,16 @@ INSERT INTO `vn`.`zoneEvent`(`zoneFk`, `type`, `started`, `ended`) VALUES (9, 'range', DATE_ADD(util.VN_CURDATE(), INTERVAL -1 YEAR), DATE_ADD(util.VN_CURDATE(), INTERVAL +1 YEAR)); -INSERT INTO `vn`.`workerTimeControl`(`userFk`, `timed`, `manual`, `direction`) +INSERT INTO `vn`.`workerTimeControl`(`userFk`, `timed`, `manual`, `direction`, `isSendMail`) VALUES - (1106, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in'), - (1106, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle'), - (1106, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle'), - (1106, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out'); + (1106, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in', 0), + (1106, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle', 0), + (1106, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle', 0), + (1106, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out', 0), + (1107, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in', 1), + (1107, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle', 1), + (1107, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle', 1), + (1107, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out', 1); INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `code`) VALUES @@ -2714,4 +2718,4 @@ UPDATE `account`.`user` INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`) VALUES - (0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', 'open', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all'); \ No newline at end of file + (0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', 'open', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all'); diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 36595c51b..b85edb5fe 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -153,6 +153,7 @@ "Email already exists": "Email already exists", "User already exists": "User already exists", "Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral", + "Record of hours week": "Registro de horas semana {{week}} año {{year}} ", "Created absence": "El empleado {{author}} ha añadido una ausencia de tipo '{{absenceType}}' a {{employee}} para el día {{dated}}.", "Deleted absence": "El empleado {{author}} ha eliminado una ausencia de tipo '{{absenceType}}' a {{employee}} del día {{dated}}.", "I have deleted the ticket id": "He eliminado el ticket id [{{id}}]({{{url}}})", @@ -237,8 +238,10 @@ "Modifiable password only via recovery or by an administrator": "Contraseña modificable solo a través de la recuperación o por un administrador", "Not enough privileges to edit a client": "No tienes suficientes privilegios para editar un cliente", "This route does not exists": "Esta ruta no existe", - "Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*", - "You don't have grant privilege": "No tienes privilegios para dar privilegios", - "You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario", + "Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*", + "You don't have grant privilege": "No tienes privilegios para dar privilegios", + "You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario", + "Already has this status": "Ya tiene este estado", + "There aren't records for this week": "No existen registros para esta semana", "Empty data source": "Origen de datos vacio" } diff --git a/modules/worker/back/methods/worker-time-control-mail/checkInbox.js b/modules/worker/back/methods/worker-time-control-mail/checkInbox.js deleted file mode 100644 index 5bb772cad..000000000 --- a/modules/worker/back/methods/worker-time-control-mail/checkInbox.js +++ /dev/null @@ -1,181 +0,0 @@ -const Imap = require('imap'); -module.exports = Self => { - Self.remoteMethod('checkInbox', { - description: 'Check an email inbox and process it', - accessType: 'READ', - returns: - { - arg: 'body', - type: 'file', - root: true - }, - http: { - path: `/checkInbox`, - verb: 'POST' - } - }); - - Self.checkInbox = async() => { - let imapConfig = await Self.app.models.WorkerTimeControlParams.findOne(); - let imap = new Imap({ - user: imapConfig.mailUser, - password: imapConfig.mailPass, - host: imapConfig.mailHost, - port: 993, - tls: true - }); - let isEmailOk; - let uid; - let emailBody; - - function openInbox(cb) { - imap.openBox('INBOX', true, cb); - } - - imap.once('ready', function() { - openInbox(function(err, box) { - if (err) throw err; - const totalMessages = box.messages.total; - if (totalMessages == 0) - imap.end(); - - let f = imap.seq.fetch('1:*', { - bodies: ['HEADER.FIELDS (FROM SUBJECT)', '1'], - struct: true - }); - f.on('message', function(msg, seqno) { - isEmailOk = false; - msg.on('body', function(stream, info) { - let buffer = ''; - let bufferCopy = ''; - stream.on('data', function(chunk) { - buffer = chunk.toString('utf8'); - if (info.which === '1' && bufferCopy.length == 0) - bufferCopy = buffer.replace(/\s/g, ' '); - }); - stream.on('end', function() { - if (bufferCopy.length > 0) { - emailBody = bufferCopy.toUpperCase().trim(); - - const bodyPositionOK = emailBody.match(/\bOK\b/i); - if (bodyPositionOK != null && (bodyPositionOK.index == 0 || bodyPositionOK.index == 122)) - isEmailOk = true; - else - isEmailOk = false; - } - }); - msg.once('attributes', function(attrs) { - uid = attrs.uid; - }); - msg.once('end', function() { - if (info.which === 'HEADER.FIELDS (FROM SUBJECT)') { - if (isEmailOk) { - imap.move(uid, 'exito', function(err) { - }); - emailConfirm(buffer); - } else { - imap.move(uid, 'error', function(err) { - }); - emailReply(buffer, emailBody); - } - } - }); - }); - }); - f.once('end', function() { - imap.end(); - }); - }); - }); - - imap.connect(); - return 'Leer emails de gestion horaria'; - }; - - async function emailConfirm(buffer) { - const now = new Date(); - const from = JSON.stringify(Imap.parseHeader(buffer).from); - const subject = JSON.stringify(Imap.parseHeader(buffer).subject); - - const timeControlDate = await getEmailDate(subject); - const week = timeControlDate[0]; - const year = timeControlDate[1]; - const user = await getUser(from); - let workerMail; - - if (user.id != null) { - workerMail = await Self.app.models.WorkerTimeControlMail.findOne({ - where: { - week: week, - year: year, - workerFk: user.id - } - }); - } - if (workerMail != null) { - await workerMail.updateAttributes({ - updated: now, - state: 'CONFIRMED' - }); - } - } - - async function emailReply(buffer, emailBody) { - const now = new Date(); - const from = JSON.stringify(Imap.parseHeader(buffer).from); - const subject = JSON.stringify(Imap.parseHeader(buffer).subject); - - const timeControlDate = await getEmailDate(subject); - const week = timeControlDate[0]; - const year = timeControlDate[1]; - const user = await getUser(from); - let workerMail; - - if (user.id != null) { - workerMail = await Self.app.models.WorkerTimeControlMail.findOne({ - where: { - week: week, - year: year, - workerFk: user.id - } - }); - - if (workerMail != null) { - await workerMail.updateAttributes({ - updated: now, - state: 'REVISE', - emailResponse: emailBody - }); - } else - await sendMail(user, subject, emailBody); - } - } - - async function getUser(workerEmail) { - const userEmail = workerEmail.match(/(?<=<)(.*?)(?=>)/); - - let [user] = await Self.rawSql(`SELECT u.id,u.name FROM account.user u - LEFT JOIN account.mailForward m on m.account = u.id - WHERE forwardTo =? OR - CONCAT(u.name,'@verdnatura.es') = ?`, - [userEmail[0], userEmail[0]]); - - return user; - } - - async function getEmailDate(subject) { - const date = subject.match(/\d+/g); - return date; - } - - async function sendMail(user, subject, emailBody) { - const sendTo = 'rrhh@verdnatura.es'; - const emailSubject = subject + ' ' + user.name; - - await Self.app.models.Mail.create({ - receiver: sendTo, - subject: emailSubject, - body: emailBody - }); - } -}; diff --git a/modules/worker/back/methods/worker-time-control/sendMail.js b/modules/worker/back/methods/worker-time-control/sendMail.js new file mode 100644 index 000000000..2f9559b3a --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/sendMail.js @@ -0,0 +1,377 @@ +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; + +module.exports = Self => { + Self.remoteMethodCtx('sendMail', { + description: `Send an email with the hours booked to the employees who telecommuting. + It also inserts booked hours in cases where the employee is telecommuting`, + accessType: 'WRITE', + accepts: [{ + arg: 'workerId', + type: 'number', + description: 'The worker id' + }, + { + arg: 'week', + type: 'number' + }, + { + arg: 'year', + type: 'number' + }], + returns: [{ + type: 'Object', + root: true + }], + http: { + path: `/sendMail`, + verb: 'POST' + } + }); + + Self.sendMail = async(ctx, options) => { + const models = Self.app.models; + const conn = Self.dataSource.connector; + const args = ctx.args; + const $t = ctx.req.__; // $translate + let tx; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + const stmts = []; + let stmt; + + try { + if (!args.week || !args.year) { + const from = new Date(); + const to = new Date(); + + const time = await models.Time.findOne({ + where: { + dated: {between: [from.setDate(from.getDate() - 10), to.setDate(to.getDate() - 4)]} + }, + order: 'week ASC' + }, myOptions); + + args.week = time.week; + args.year = time.year; + } + + const started = getStartDateOfWeekNumber(args.week, args.year); + started.setHours(0, 0, 0, 0); + + const ended = new Date(started); + ended.setDate(started.getDate() + 6); + ended.setHours(23, 59, 59, 999); + + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate'); + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeBusinessCalculate'); + + if (args.workerId) { + await models.WorkerTimeControl.destroyAll({ + userFk: args.workerId, + timed: {between: [started, ended]}, + isSendMail: true + }, myOptions); + + const where = { + workerFk: args.workerId, + year: args.year, + week: args.week + }; + await models.WorkerTimeControlMail.updateAll(where, { + updated: new Date(), state: 'SENDED' + }, myOptions); + + stmt = new ParameterizedSQL( + `CALL vn.timeControl_calculateByUser(?, ?, ?) + `, [args.workerId, started, ended]); + stmts.push(stmt); + + stmt = new ParameterizedSQL( + `CALL vn.timeBusiness_calculateByUser(?, ?, ?) + `, [args.workerId, started, ended]); + stmts.push(stmt); + } else { + await models.WorkerTimeControl.destroyAll({ + timed: {between: [started, ended]}, + isSendMail: true + }, myOptions); + + const where = { + year: args.year, + week: args.week + }; + await models.WorkerTimeControlMail.updateAll(where, { + updated: new Date(), state: 'SENDED' + }, myOptions); + + stmt = new ParameterizedSQL(`CALL vn.timeControl_calculateAll(?, ?)`, [started, ended]); + stmts.push(stmt); + + stmt = new ParameterizedSQL(`CALL vn.timeBusiness_calculateAll(?, ?)`, [started, ended]); + stmts.push(stmt); + } + + stmt = new ParameterizedSQL(` + SELECT CONCAT(u.name, '@verdnatura.es') receiver, + u.id workerFk, + tb.dated, + tb.timeWorkDecimal, + tb.timeWorkSexagesimal timeWorkSexagesimal, + tb.timeTable, + tc.timeWorkDecimal timeWorkedDecimal, + tc.timeWorkSexagesimal timeWorkedSexagesimal, + tb.type, + tb.businessFk, + tb.permissionRate, + d.isTeleworking + FROM tmp.timeBusinessCalculate tb + JOIN user u ON u.id = tb.userFk + JOIN department d ON d.id = tb.departmentFk + JOIN business b ON b.id = tb.businessFk + LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk AND tc.dated = tb.dated + LEFT JOIN worker w ON w.id = u.id + JOIN (SELECT tb.userFk, + SUM(IF(tb.type IS NULL, + IF(tc.timeWorkDecimal > 0, FALSE, IF(tb.timeWorkDecimal > 0, TRUE, FALSE)), + TRUE))isTeleworkingWeek + FROM tmp.timeBusinessCalculate tb + LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk + AND tc.dated = tb.dated + GROUP BY tb.userFk + HAVING isTeleworkingWeek > 0 + )sub ON sub.userFk = u.id + WHERE d.hasToRefill + AND IFNULL(?, u.id) = u.id + AND b.companyCodeFk = 'VNL' + AND w.businessFk + ORDER BY u.id, tb.dated + `, [args.workerId]); + const index = stmts.push(stmt) - 1; + + const sql = ParameterizedSQL.join(stmts, ';'); + const days = await conn.executeStmt(sql, myOptions); + + let previousWorkerFk = days[index][0].workerFk; + let previousReceiver = days[index][0].receiver; + + const workerTimeControlConfig = await models.WorkerTimeControlConfig.findOne(null, myOptions); + + for (let day of days[index]) { + workerFk = day.workerFk; + if (day.timeWorkDecimal > 0 && day.timeWorkedDecimal == null + && (day.permissionRate ? day.permissionRate : true)) { + if (day.timeTable == null) { + const timed = new Date(day.dated); + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(8), + manual: true, + direction: 'in', + isSendMail: true + }, myOptions); + + if (day.timeWorkDecimal >= workerTimeControlConfig.timeToBreakTime / 3600) { + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(9), + manual: true, + direction: 'middle', + isSendMail: true + }, myOptions); + + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(9, 20), + manual: true, + direction: 'middle', + isSendMail: true + }, myOptions); + } + + const [hoursWork, minutesWork, secondsWork] = getTime(day.timeWorkSexagesimal); + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(8 + hoursWork, minutesWork, secondsWork), + manual: true, + direction: 'out', + isSendMail: true + }, myOptions); + } else { + const weekDay = day.dated.getDay(); + const journeys = await models.Journey.find({ + where: { + business_id: day.businessFk, + day_id: weekDay + } + }, myOptions); + + let timeTableDecimalInSeconds = 0; + for (let journey of journeys) { + const start = new Date(); + const [startHours, startMinutes, startSeconds] = getTime(journey.start); + start.setHours(startHours, startMinutes, startSeconds, 0); + + const end = new Date(); + const [endHours, endMinutes, endSeconds] = getTime(journey.end); + end.setHours(endHours, endMinutes, endSeconds, 0); + + const result = (end - start) / 1000; + timeTableDecimalInSeconds += result; + } + + for (let journey of journeys) { + const timeTableDecimal = timeTableDecimalInSeconds / 3600; + if (day.timeWorkDecimal == timeTableDecimal) { + const timed = new Date(day.dated); + const [startHours, startMinutes, startSeconds] = getTime(journey.start); + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(startHours, startMinutes, startSeconds), + manual: true, + isSendMail: true + }, myOptions); + + const [endHours, endMinutes, endSeconds] = getTime(journey.end); + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(endHours, endMinutes, endSeconds), + manual: true, + isSendMail: true + }, myOptions); + } else { + const minStart = journeys.reduce(function(prev, curr) { + return curr.start < prev.start ? curr : prev; + }); + if (journey == minStart) { + const timed = new Date(day.dated); + const [startHours, startMinutes, startSeconds] = getTime(journey.start); + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(startHours, startMinutes, startSeconds), + manual: true, + isSendMail: true + }, myOptions); + + const [hoursWork, minutesWork, secondsWork] = getTime(day.timeWorkSexagesimal); + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours( + startHours + hoursWork, + startMinutes + minutesWork, + startSeconds + secondsWork + ), + manual: true, + isSendMail: true + }, myOptions); + } + } + + if (day.timeWorkDecimal >= workerTimeControlConfig.timeToBreakTime / 3600) { + const minStart = journeys.reduce(function(prev, curr) { + return curr.start < prev.start ? curr : prev; + }); + if (journey == minStart) { + const timed = new Date(day.dated); + const [startHours, startMinutes, startSeconds] = getTime(journey.start); + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(startHours + 1, startMinutes, startSeconds), + manual: true, + isSendMail: true + }, myOptions); + + await models.WorkerTimeControl.create({ + userFk: day.workerFk, + timed: timed.setHours(startHours + 1, startMinutes + 20, startSeconds), + manual: true, + isSendMail: true + }, myOptions); + } + } + } + const timed = new Date(day.dated); + const firstWorkerTimeControl = await models.WorkerTimeControl.findOne({ + where: { + userFk: day.workerFk, + timed: {between: [timed.setHours(0, 0, 0, 0), timed.setHours(23, 59, 59, 999)]} + }, + order: 'timed ASC' + }, myOptions); + + if (firstWorkerTimeControl) + firstWorkerTimeControl.updateAttribute('direction', 'in', myOptions); + + const lastWorkerTimeControl = await models.WorkerTimeControl.findOne({ + where: { + userFk: day.workerFk, + timed: {between: [timed.setHours(0, 0, 0, 0), timed.setHours(23, 59, 59, 999)]} + }, + order: 'timed DESC' + }, myOptions); + + if (lastWorkerTimeControl) + lastWorkerTimeControl.updateAttribute('direction', 'out', myOptions); + } + } + + const lastDay = days[index][days[index].length - 1]; + if (day.workerFk != previousWorkerFk || day == lastDay) { + const salix = await models.Url.findOne({ + where: { + appName: 'salix', + environment: process.env.NODE_ENV || 'dev' + } + }, myOptions); + + const timestamp = started.getTime() / 1000; + await models.Mail.create({ + receiver: previousReceiver, + subject: $t('Record of hours week', { + week: args.week, + year: args.year + }), + body: `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}` + }, myOptions); + + query = `INSERT IGNORE INTO workerTimeControlMail (workerFk, year, week) + VALUES (?, ?, ?);`; + await Self.rawSql(query, [previousWorkerFk, args.year, args.week], myOptions); + + previousWorkerFk = day.workerFk; + previousReceiver = day.receiver; + } + } + + if (tx) await tx.commit(); + + return true; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; + + function getStartDateOfWeekNumber(week, year) { + const simple = new Date(year, 0, 1 + (week - 1) * 7); + const dow = simple.getDay(); + const weekStart = simple; + if (dow <= 4) + weekStart.setDate(simple.getDate() - simple.getDay() + 1); + else + weekStart.setDate(simple.getDate() + 8 - simple.getDay()); + return weekStart; + } + + function getTime(timeString) { + const [hours, minutes, seconds] = timeString.split(':'); + return [parseInt(hours), parseInt(minutes), parseInt(seconds)]; + } +}; diff --git a/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js b/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js new file mode 100644 index 000000000..d0afd45b9 --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js @@ -0,0 +1,132 @@ +const models = require('vn-loopback/server/server').models; + +describe('workerTimeControl sendMail()', () => { + const workerId = 18; + const ctx = { + req: { + __: value => { + return value; + } + }, + args: {} + + }; + + beforeAll(function() { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + }); + + it('should fill time control of a worker without records in Journey and with rest', async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + + try { + const options = {transaction: tx}; + + await models.WorkerTimeControl.sendMail(ctx, options); + + const workerTimeControl = await models.WorkerTimeControl.find({ + where: {userFk: workerId} + }, options); + + expect(workerTimeControl[0].timed.getHours()).toEqual(8); + expect(workerTimeControl[1].timed.getHours()).toEqual(9); + expect(`${workerTimeControl[2].timed.getHours()}:${workerTimeControl[2].timed.getMinutes()}`).toEqual('9:20'); + expect(workerTimeControl[3].timed.getHours()).toEqual(16); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should fill time control of a worker without records in Journey and without rest', async() => { + const workdayOf20Hours = 3; + const tx = await models.WorkerTimeControl.beginTransaction({}); + + try { + const options = {transaction: tx}; + query = `UPDATE business b + SET b.calendarTypeFk = ? + WHERE b.workerFk = ?; `; + await models.WorkerTimeControl.rawSql(query, [workdayOf20Hours, workerId], options); + + await models.WorkerTimeControl.sendMail(ctx, options); + + const workerTimeControl = await models.WorkerTimeControl.find({ + where: {userFk: workerId} + }, options); + + expect(workerTimeControl[0].timed.getHours()).toEqual(8); + expect(workerTimeControl[1].timed.getHours()).toEqual(12); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should fill time control of a worker with records in Journey and with rest', async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + + try { + const options = {transaction: tx}; + query = `INSERT INTO postgresql.journey(journey_id, day_id, start, end, business_id) + VALUES + (1, 1, '09:00:00', '13:00:00', ?), + (2, 1, '14:00:00', '19:00:00', ?);`; + await models.WorkerTimeControl.rawSql(query, [workerId, workerId, workerId], options); + + await models.WorkerTimeControl.sendMail(ctx, options); + + const workerTimeControl = await models.WorkerTimeControl.find({ + where: {userFk: workerId} + }, options); + + expect(workerTimeControl[0].timed.getHours()).toEqual(9); + expect(workerTimeControl[2].timed.getHours()).toEqual(10); + expect(`${workerTimeControl[3].timed.getHours()}:${workerTimeControl[3].timed.getMinutes()}`).toEqual('10:20'); + expect(workerTimeControl[1].timed.getHours()).toEqual(13); + expect(workerTimeControl[4].timed.getHours()).toEqual(14); + expect(workerTimeControl[5].timed.getHours()).toEqual(19); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should fill time control of a worker with records in Journey and without rest', async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + + try { + const options = {transaction: tx}; + query = `INSERT INTO postgresql.journey(journey_id, day_id, start, end, business_id) + VALUES + (1, 1, '12:30:00', '14:00:00', ?);`; + await models.WorkerTimeControl.rawSql(query, [workerId, workerId, workerId], options); + + await models.WorkerTimeControl.sendMail(ctx, options); + + const workerTimeControl = await models.WorkerTimeControl.find({ + where: {userFk: workerId} + }, options); + + expect(`${workerTimeControl[0].timed.getHours()}:${workerTimeControl[0].timed.getMinutes()}`).toEqual('12:30'); + expect(workerTimeControl[1].timed.getHours()).toEqual(14); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + afterAll(function() { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); +}); + diff --git a/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js b/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js new file mode 100644 index 000000000..a8dc14bb1 --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js @@ -0,0 +1,87 @@ +const UserError = require('vn-loopback/util/user-error'); +module.exports = Self => { + Self.remoteMethodCtx('updateWorkerTimeControlMail', { + description: 'Updates the state of WorkerTimeControlMail', + accessType: 'WRITE', + accepts: [{ + arg: 'workerId', + type: 'number', + required: true + }, + { + arg: 'year', + type: 'number', + required: true + }, + { + arg: 'week', + type: 'number', + required: true + }, + { + arg: 'state', + type: 'string', + required: true + }, + { + arg: 'reason', + type: 'string' + }], + returns: { + type: 'boolean', + root: true + }, + http: { + path: `/updateWorkerTimeControlMail`, + verb: 'POST' + } + }); + + Self.updateWorkerTimeControlMail = async(ctx, options) => { + const models = Self.app.models; + const args = ctx.args; + const userId = ctx.req.accessToken.userId; + + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const workerTimeControlMail = await models.WorkerTimeControlMail.findOne({ + where: { + workerFk: args.workerId, + year: args.year, + week: args.week + } + }, myOptions); + + if (!workerTimeControlMail) throw new UserError(`There aren't records for this week`); + + const oldState = workerTimeControlMail.state; + const oldReason = workerTimeControlMail.reason; + + if (oldState == args.state) throw new UserError('Already has this status'); + + await workerTimeControlMail.updateAttributes({ + state: args.state, + reason: args.reason || null + }, myOptions); + + const logRecord = { + originFk: args.workerId, + userFk: userId, + action: 'update', + changedModel: 'WorkerTimeControlMail', + oldInstance: { + state: oldState, + reason: oldReason + }, + newInstance: { + state: args.state, + reason: args.reason + } + }; + + return models.WorkerLog.create(logRecord, myOptions); + }; +}; diff --git a/modules/worker/back/model-config.json b/modules/worker/back/model-config.json index 8c11c0d71..3f3416504 100644 --- a/modules/worker/back/model-config.json +++ b/modules/worker/back/model-config.json @@ -20,6 +20,12 @@ "EducationLevel": { "dataSource": "vn" }, + "Journey": { + "dataSource": "vn" + }, + "Time": { + "dataSource": "vn" + }, "WorkCenter": { "dataSource": "vn" }, @@ -59,6 +65,9 @@ "WorkerLog": { "dataSource": "vn" }, + "WorkerTimeControlConfig": { + "dataSource": "vn" + }, "WorkerTimeControlParams": { "dataSource": "vn" }, diff --git a/modules/worker/back/models/journey.json b/modules/worker/back/models/journey.json new file mode 100644 index 000000000..b7d5f2817 --- /dev/null +++ b/modules/worker/back/models/journey.json @@ -0,0 +1,27 @@ +{ + "name": "Journey", + "base": "VnModel", + "options": { + "mysql": { + "table": "postgresql.journey" + } + }, + "properties": { + "journey_id": { + "id": true, + "type": "number" + }, + "day_id": { + "type": "number" + }, + "start": { + "type": "date" + }, + "end": { + "type": "date" + }, + "business_id": { + "type": "number" + } + } +} diff --git a/modules/worker/back/models/time.json b/modules/worker/back/models/time.json new file mode 100644 index 000000000..df9257540 --- /dev/null +++ b/modules/worker/back/models/time.json @@ -0,0 +1,21 @@ +{ + "name": "Time", + "base": "VnModel", + "options": { + "mysql": { + "table": "time" + } + }, + "properties": { + "dated": { + "id": true, + "type": "date" + }, + "year": { + "type": "number" + }, + "week": { + "type": "number" + } + } +} diff --git a/modules/worker/back/models/worker-time-control-config.json b/modules/worker/back/models/worker-time-control-config.json new file mode 100644 index 000000000..4c12ce5d7 --- /dev/null +++ b/modules/worker/back/models/worker-time-control-config.json @@ -0,0 +1,18 @@ +{ + "name": "WorkerTimeControlConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "workerTimeControlConfig" + } + }, + "properties": { + "id": { + "id": true, + "type": "number" + }, + "timeToBreakTime": { + "type": "number" + } + } +} \ No newline at end of file diff --git a/modules/worker/back/models/worker-time-control-mail.js b/modules/worker/back/models/worker-time-control-mail.js deleted file mode 100644 index 36f3851b6..000000000 --- a/modules/worker/back/models/worker-time-control-mail.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = Self => { - require('../methods/worker-time-control-mail/checkInbox')(Self); -}; diff --git a/modules/worker/back/models/worker-time-control-mail.json b/modules/worker/back/models/worker-time-control-mail.json index daf3d5155..78b99881d 100644 --- a/modules/worker/back/models/worker-time-control-mail.json +++ b/modules/worker/back/models/worker-time-control-mail.json @@ -9,8 +9,7 @@ "properties": { "id": { "id": true, - "type": "number", - "required": true + "type": "number" }, "workerFk": { "type": "number" @@ -27,7 +26,7 @@ "updated": { "type": "date" }, - "emailResponse": { + "reason": { "type": "string" } }, diff --git a/modules/worker/back/models/worker-time-control.js b/modules/worker/back/models/worker-time-control.js index 45f4e2194..9f802511a 100644 --- a/modules/worker/back/models/worker-time-control.js +++ b/modules/worker/back/models/worker-time-control.js @@ -5,6 +5,8 @@ module.exports = Self => { require('../methods/worker-time-control/addTimeEntry')(Self); require('../methods/worker-time-control/deleteTimeEntry')(Self); require('../methods/worker-time-control/updateTimeEntry')(Self); + require('../methods/worker-time-control/sendMail')(Self); + require('../methods/worker-time-control/updateWorkerTimeControlMail')(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 ab07802ca..bc3e53501 100644 --- a/modules/worker/back/models/worker-time-control.json +++ b/modules/worker/back/models/worker-time-control.json @@ -22,6 +22,9 @@ }, "direction": { "type": "string" + }, + "isSendMail": { + "type": "boolean" } }, "relations": { diff --git a/modules/worker/front/time-control/index.html b/modules/worker/front/time-control/index.html index 170d88b21..681d420d0 100644 --- a/modules/worker/front/time-control/index.html +++ b/modules/worker/front/time-control/index.html @@ -77,6 +77,18 @@ + + + + + + + +
@@ -148,4 +160,21 @@ ng-click="$ctrl.save()"> - \ 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 ebf70e886..c3d3e5eab 100644 --- a/modules/worker/front/time-control/index.js +++ b/modules/worker/front/time-control/index.js @@ -294,6 +294,42 @@ class Controller extends Section { this.$.editEntry.show($event); } + getWeekNumber(currentDate) { + const startDate = new Date(currentDate.getFullYear(), 0, 1); + let days = Math.floor((currentDate - startDate) / + (24 * 60 * 60 * 1000)); + return Math.ceil(days / 7); + } + + isSatisfied() { + const weekNumber = this.getWeekNumber(this.date); + const params = { + workerId: this.worker.id, + year: this.date.getFullYear(), + week: weekNumber, + state: 'CONFIRMED' + }; + const query = `WorkerTimeControls/updateWorkerTimeControlMail`; + this.$http.post(query, params).then(() => { + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } + + isUnsatisfied() { + const weekNumber = this.getWeekNumber(this.date); + const params = { + workerId: this.worker.id, + year: this.date.getFullYear(), + week: weekNumber, + state: 'REVISE', + reason: this.reason + }; + const query = `WorkerTimeControls/updateWorkerTimeControlMail`; + this.$http.post(query, params).then(() => { + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } + save() { try { const entry = this.selectedRow; diff --git a/modules/worker/front/time-control/locale/es.yml b/modules/worker/front/time-control/locale/es.yml index 8b2486f05..2a3bffc00 100644 --- a/modules/worker/front/time-control/locale/es.yml +++ b/modules/worker/front/time-control/locale/es.yml @@ -10,4 +10,7 @@ This time entry will be deleted: Se eliminará la hora fichada Are you sure you want to delete this entry?: ¿Seguro que quieres eliminarla? Finish at: Termina a las Entry removed: Fichada borrada -The entry type can't be empty: El tipo de fichada no puede quedar vacía \ No newline at end of file +The entry type can't be empty: El tipo de fichada no puede quedar vacía +Satisfied: Conforme +Not satisfied: No conforme +Reason: Motivo \ No newline at end of file