diff --git a/back/methods/account/recover-password.js b/back/methods/account/recover-password.js new file mode 100644 index 000000000..ddea76829 --- /dev/null +++ b/back/methods/account/recover-password.js @@ -0,0 +1,30 @@ +module.exports = Self => { + Self.remoteMethod('recoverPassword', { + description: 'Send email to the user', + accepts: [ + { + arg: 'email', + type: 'string', + description: 'The email of user', + required: true + } + ], + http: { + path: `/recoverPassword`, + verb: 'POST' + } + }); + + Self.recoverPassword = async function(email) { + const models = Self.app.models; + + try { + await models.user.resetPassword({email, emailTemplate: 'recover-password'}); + } catch (err) { + if (err.code === 'EMAIL_NOT_FOUND') + return; + else + throw err; + } + }; +}; diff --git a/back/methods/account/specs/set-password.spec.js b/back/methods/account/specs/set-password.spec.js index c76fd52b8..fe71873de 100644 --- a/back/methods/account/specs/set-password.spec.js +++ b/back/methods/account/specs/set-password.spec.js @@ -1,6 +1,6 @@ const app = require('vn-loopback/server/server'); -describe('account changePassword()', () => { +describe('account setPassword()', () => { it('should throw an error when password does not meet requirements', async() => { let req = app.models.Account.setPassword(1, 'insecurePass'); diff --git a/back/methods/chat/notifyIssues.js b/back/methods/chat/notifyIssues.js index 3c4706d07..f618a6fb1 100644 --- a/back/methods/chat/notifyIssues.js +++ b/back/methods/chat/notifyIssues.js @@ -32,7 +32,7 @@ module.exports = Self => { let message = $t(`There's a new urgent ticket:`); const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id='; tickets.forEach(ticket => { - message += `\r\n[ID: *${ticket.number}* - ${ticket.subject} @${ticket.username}](${ostUri + ticket.id})`; + message += `\r\n[ID: ${ticket.number} - ${ticket.subject} @${ticket.username}](${ostUri + ticket.id})`; }); const department = await models.Department.findOne({ diff --git a/back/methods/chat/spec/notifyIssue.spec.js b/back/methods/chat/spec/notifyIssue.spec.js index e20d43142..1aab51793 100644 --- a/back/methods/chat/spec/notifyIssue.spec.js +++ b/back/methods/chat/spec/notifyIssue.spec.js @@ -27,7 +27,7 @@ describe('Chat notifyIssue()', () => { subject: 'Issue title'} ]); // eslint-disable-next-line max-len - const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: *00001* - Issue title (@batman)](https://cau.verdnatura.es/scp/tickets.php?id=1)`; + const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: 00001 - Issue title @batman](https://cau.verdnatura.es/scp/tickets.php?id=1)`; const department = await app.models.Department.findById(departmentId); let orgChatName = department.chatName; diff --git a/back/methods/dms/deleteTrashFiles.js b/back/methods/dms/deleteTrashFiles.js index 63d7021c5..f14e65e9f 100644 --- a/back/methods/dms/deleteTrashFiles.js +++ b/back/methods/dms/deleteTrashFiles.js @@ -51,7 +51,7 @@ module.exports = Self => { const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file); await fs.unlink(dstFile); } catch (err) { - if (err.code != 'ENOENT') + if (err.code != 'ENOENT' && dms.file) throw err; } diff --git a/back/models/account.js b/back/models/account.js index f74052b5c..c2502380a 100644 --- a/back/models/account.js +++ b/back/models/account.js @@ -1,4 +1,7 @@ +/* eslint max-len: ["error", { "code": 150 }]*/ const md5 = require('md5'); +const LoopBackContext = require('loopback-context'); +const {Email} = require('vn-print'); module.exports = Self => { require('../methods/account/login')(Self); @@ -6,6 +9,7 @@ module.exports = Self => { require('../methods/account/acl')(Self); require('../methods/account/change-password')(Self); require('../methods/account/set-password')(Self); + require('../methods/account/recover-password')(Self); require('../methods/account/validate-token')(Self); require('../methods/account/privileges')(Self); @@ -27,17 +31,62 @@ module.exports = Self => { ctx.data.password = md5(ctx.data.password); }); + Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => { + if (!ctx.args || !ctx.args.data.email) return; + const models = Self.app.models; + + const loopBackContext = LoopBackContext.getCurrentContext(); + const httpCtx = {req: loopBackContext.active}; + const httpRequest = httpCtx.req.http.req; + const headers = httpRequest.headers; + const origin = headers.origin; + const url = origin.split(':'); + + const userId = ctx.instance.id; + const user = await models.user.findById(userId); + + class Mailer { + async send(verifyOptions, cb) { + const params = { + url: verifyOptions.verifyHref, + recipient: verifyOptions.to, + lang: ctx.req.getLocale() + }; + + const email = new Email('email-verify', params); + email.send(); + + cb(null, verifyOptions.to); + } + } + + const options = { + type: 'email', + to: instance.email, + from: {}, + redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`, + template: false, + mailer: new Mailer, + host: url[1].split('/')[2], + port: url[2], + protocol: url[0], + user: Self + }; + + await user.verify(options); + }); + Self.remoteMethod('getCurrentUserData', { description: 'Gets the current user data', accepts: [ { arg: 'ctx', - type: 'Object', + type: 'object', http: {source: 'context'} } ], returns: { - type: 'Object', + type: 'object', root: true }, http: { @@ -58,7 +107,7 @@ module.exports = Self => { * * @param {Integer} userId The user id * @param {String} name The role name - * @param {Object} options Options + * @param {object} options Options * @return {Boolean} %true if user has the role, %false otherwise */ Self.hasRole = async function(userId, name, options) { @@ -70,8 +119,8 @@ module.exports = Self => { * Get all user roles. * * @param {Integer} userId The user id - * @param {Object} options Options - * @return {Object} User role list + * @param {object} options Options + * @return {object} User role list */ Self.getRoles = async(userId, options) => { let result = await Self.rawSql( diff --git a/back/models/account.json b/back/models/account.json index d0c17e70f..5e35c711a 100644 --- a/back/models/account.json +++ b/back/models/account.json @@ -40,6 +40,9 @@ "email": { "type": "string" }, + "emailVerified": { + "type": "boolean" + }, "created": { "type": "date" }, @@ -88,16 +91,23 @@ "principalType": "ROLE", "principalId": "$everyone", "permission": "ALLOW" - }, + }, + { + "property": "recoverPassword", + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + }, { - "property": "logout", + "property": "logout", "accessType": "EXECUTE", "principalType": "ROLE", "principalId": "$authenticated", "permission": "ALLOW" }, { - "property": "validateToken", + "property": "validateToken", "accessType": "EXECUTE", "principalType": "ROLE", "principalId": "$authenticated", diff --git a/back/models/chat.js b/back/models/chat.js index f45d15180..a18edbd3f 100644 --- a/back/models/chat.js +++ b/back/models/chat.js @@ -11,9 +11,11 @@ module.exports = Self => { let {message} = ctx.instance; if (!message) return; - const parts = message.match(/(?<=\[).*(?=])/g); + const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g); + if (!parts) return; + const replacedParts = parts.map(part => { - return part.replace(/[*()]/g, ''); + return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, ''); }); for (const [index, part] of parts.entries()) diff --git a/back/models/specs/account.spec.js b/back/models/specs/account.spec.js index c52bc4378..f31c81b75 100644 --- a/back/models/specs/account.spec.js +++ b/back/models/specs/account.spec.js @@ -1,14 +1,14 @@ -const app = require('vn-loopback/server/server'); +const models = require('vn-loopback/server/server').models; describe('loopback model Account', () => { it('should return true if the user has the given role', async() => { - let result = await app.models.Account.hasRole(1, 'employee'); + let result = await models.Account.hasRole(1, 'employee'); expect(result).toBeTruthy(); }); it('should return false if the user doesnt have the given role', async() => { - let result = await app.models.Account.hasRole(1, 'administrator'); + let result = await models.Account.hasRole(1, 'administrator'); expect(result).toBeFalsy(); }); diff --git a/back/models/specs/user.spec.js b/back/models/specs/user.spec.js new file mode 100644 index 000000000..124afdc0c --- /dev/null +++ b/back/models/specs/user.spec.js @@ -0,0 +1,32 @@ +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); + +describe('account recoverPassword()', () => { + const userId = 1107; + + const activeCtx = { + accessToken: {userId: userId}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; + + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + + it('should send email with token', async() => { + const userId = 1107; + const user = await models.Account.findById(userId); + + await models.Account.recoverPassword(user.email); + + const result = await models.AccessToken.findOne({where: {userId: userId}}); + + expect(result).toBeDefined(); + }); +}); diff --git a/back/models/user.js b/back/models/user.js new file mode 100644 index 000000000..b24d702b3 --- /dev/null +++ b/back/models/user.js @@ -0,0 +1,27 @@ +const LoopBackContext = require('loopback-context'); +const {Email} = require('vn-print'); + +module.exports = function(Self) { + Self.on('resetPasswordRequest', async function(info) { + const loopBackContext = LoopBackContext.getCurrentContext(); + const httpCtx = {req: loopBackContext.active}; + const httpRequest = httpCtx.req.http.req; + const headers = httpRequest.headers; + const origin = headers.origin; + + const user = await Self.app.models.Account.findById(info.user.id); + const params = { + recipient: info.email, + lang: user.lang, + url: `${origin}/#!/reset-password?access_token=${info.accessToken.id}` + }; + + const options = Object.assign({}, info.options); + for (const param in options) + params[param] = options[param]; + + const email = new Email(options.emailTemplate, params); + + return email.send(); + }); +}; diff --git a/db/changes/10502-november/00-aclUserPassword.sql b/db/changes/10502-november/00-aclUserPassword.sql new file mode 100644 index 000000000..b92b54c28 --- /dev/null +++ b/db/changes/10502-november/00-aclUserPassword.sql @@ -0,0 +1,2 @@ +DELETE FROM `salix`.`ACL` + WHERE model = 'UserPassword'; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index dd037bfae..8e2ecc809 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2564,10 +2564,6 @@ UPDATE `vn`.`route` UPDATE `vn`.`route` SET `invoiceInFk`=2 WHERE `id`=2; -INSERT INTO `bs`.`salesPerson` (`workerFk`, `year`, `month`, `portfolioWeight`) - VALUES - (18, YEAR(util.VN_CURDATE()), MONTH(util.VN_CURDATE()), 807.23), - (19, YEAR(util.VN_CURDATE()), MONTH(util.VN_CURDATE()), 34.40); INSERT INTO `bs`.`sale` (`saleFk`, `amount`, `dated`, `typeFk`, `clientFk`) VALUES diff --git a/db/dump/structure.sql b/db/dump/structure.sql index 6d85d0511..403534787 100644 --- a/db/dump/structure.sql +++ b/db/dump/structure.sql @@ -81044,4 +81044,3 @@ USE `vncontrol`; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2022-09-16 10:44:31 - diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 06fabe3e6..f550e3a9d 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -29,6 +29,11 @@ export default { firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]', firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]' }, + recoverPassword: { + recoverPasswordButton: 'vn-login a[ui-sref="recoverPassword"]', + email: 'vn-recover-password vn-textfield[ng-model="$ctrl.email"]', + sendEmailButton: 'vn-recover-password vn-submit', + }, accountIndex: { addAccount: 'vn-user-index button vn-icon[icon="add"]', newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]', diff --git a/e2e/paths/01-salix/04_recoverPassword.spec.js b/e2e/paths/01-salix/04_recoverPassword.spec.js new file mode 100644 index 000000000..80ef32cb5 --- /dev/null +++ b/e2e/paths/01-salix/04_recoverPassword.spec.js @@ -0,0 +1,40 @@ +import selectors from '../../helpers/selectors'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Login path', async() => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + + await page.waitToClick(selectors.recoverPassword.recoverPasswordButton); + await page.waitForState('recoverPassword'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should not throw error if not exist user', async() => { + await page.write(selectors.recoverPassword.email, 'fakeEmail@mydomain.com'); + await page.waitToClick(selectors.recoverPassword.sendEmailButton); + + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Notification sent!'); + }); + + it('should send email', async() => { + await page.waitForState('login'); + await page.waitToClick(selectors.recoverPassword.recoverPasswordButton); + + await page.write(selectors.recoverPassword.email, 'BruceWayne@mydomain.com'); + await page.waitToClick(selectors.recoverPassword.sendEmailButton); + const message = await page.waitForSnackbar(); + await page.waitForState('login'); + + expect(message.text).toContain('Notification sent!'); + }); +}); diff --git a/e2e/paths/05-ticket/20_future.spec.js b/e2e/paths/05-ticket/20_future.spec.js index 4fee9523b..6db2bf4f0 100644 --- a/e2e/paths/05-ticket/20_future.spec.js +++ b/e2e/paths/05-ticket/20_future.spec.js @@ -5,80 +5,78 @@ describe('Ticket Future path', () => { let browser; let page; - beforeAll(async () => { + beforeAll(async() => { browser = await getBrowser(); page = browser.page; await page.loginAndModule('employee', 'ticket'); await page.accessToSection('ticket.future'); }); - afterAll(async () => { + afterAll(async() => { await browser.close(); }); const now = new Date(); const tomorrow = new Date(now.getDate() + 1); - const ticket = { - originDated: now, - futureDated: now, - linesMax: '9999', - litersMax: '9999', - warehouseFk: 'Warehouse One' - }; - it('should show errors snackbar because of the required data', async () => { + it('should show errors snackbar because of the required data', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.warehouseFk); await page.waitToClick(selectors.ticketFuture.submit); let message = await page.waitForSnackbar(); + expect(message.text).toContain('warehouseFk is a required argument'); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.litersMax); await page.waitToClick(selectors.ticketFuture.submit); message = await page.waitForSnackbar(); + expect(message.text).toContain('litersMax is a required argument'); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.linesMax); await page.waitToClick(selectors.ticketFuture.submit); message = await page.waitForSnackbar(); + expect(message.text).toContain('linesMax is a required argument'); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.futureDated); await page.waitToClick(selectors.ticketFuture.submit); message = await page.waitForSnackbar(); + expect(message.text).toContain('futureDated is a required argument'); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.originDated); await page.waitToClick(selectors.ticketFuture.submit); message = await page.waitForSnackbar(); + expect(message.text).toContain('originDated is a required argument'); }); - it('should search with the required data', async () => { + it('should search with the required data', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.submit); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search with the origin shipped today', async () => { + it('should search with the origin shipped today', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.pickDate(selectors.ticketFuture.shipped, now); await page.waitToClick(selectors.ticketFuture.submit); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search with the origin shipped tomorrow', async () => { + it('should search with the origin shipped tomorrow', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.pickDate(selectors.ticketFuture.shipped, tomorrow); await page.waitToClick(selectors.ticketFuture.submit); await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); }); - it('should search with the destination shipped today', async () => { + it('should search with the destination shipped today', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.shipped); await page.pickDate(selectors.ticketFuture.tfShipped, now); @@ -86,14 +84,14 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search with the destination shipped tomorrow', async () => { + it('should search with the destination shipped tomorrow', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow); await page.waitToClick(selectors.ticketFuture.submit); await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); }); - it('should search with the origin IPT', async () => { + it('should search with the origin IPT', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.shipped); @@ -108,7 +106,7 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); }); - it('should search with the destination IPT', async () => { + it('should search with the destination IPT', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.shipped); @@ -123,7 +121,7 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); }); - it('should search with the origin grouped state', async () => { + it('should search with the origin grouped state', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.shipped); @@ -138,7 +136,7 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 3); }); - it('should search with the destination grouped state', async () => { + it('should search with the destination grouped state', async() => { await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.clearInput(selectors.ticketFuture.shipped); @@ -164,10 +162,10 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search in smart-table with an ID Origin', async () => { + it('should search in smart-table with an ID Origin', async() => { await page.waitToClick(selectors.ticketFuture.tableButtonSearch); - await page.write(selectors.ticketFuture.tableId, "13"); - await page.keyboard.press("Enter"); + await page.write(selectors.ticketFuture.tableId, '13'); + await page.keyboard.press('Enter'); await page.waitForNumberOfElements(selectors.ticketFuture.table, 2); await page.waitToClick(selectors.ticketFuture.tableButtonSearch); @@ -176,10 +174,10 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search in smart-table with an ID Destination', async () => { + it('should search in smart-table with an ID Destination', async() => { await page.waitToClick(selectors.ticketFuture.tableButtonSearch); - await page.write(selectors.ticketFuture.tableTfId, "12"); - await page.keyboard.press("Enter"); + await page.write(selectors.ticketFuture.tableTfId, '12'); + await page.keyboard.press('Enter'); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitToClick(selectors.ticketFuture.tableButtonSearch); @@ -188,7 +186,7 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search in smart-table with an IPT Origin', async () => { + it('should search in smart-table with an IPT Origin', async() => { await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical'); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); @@ -199,7 +197,7 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search in smart-table with an IPT Destination', async () => { + it('should search in smart-table with an IPT Destination', async() => { await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical'); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); @@ -210,10 +208,10 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search in smart-table with especified Lines', async () => { + it('should search in smart-table with especified Lines', async() => { await page.waitToClick(selectors.ticketFuture.tableButtonSearch); - await page.write(selectors.ticketFuture.tableLines, "0"); - await page.keyboard.press("Enter"); + await page.write(selectors.ticketFuture.tableLines, '0'); + await page.keyboard.press('Enter'); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitToClick(selectors.ticketFuture.tableButtonSearch); @@ -222,8 +220,8 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitToClick(selectors.ticketFuture.tableButtonSearch); - await page.write(selectors.ticketFuture.tableLines, "1"); - await page.keyboard.press("Enter"); + await page.write(selectors.ticketFuture.tableLines, '1'); + await page.keyboard.press('Enter'); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitToClick(selectors.ticketFuture.tableButtonSearch); @@ -232,10 +230,10 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should search in smart-table with especified Liters', async () => { + it('should search in smart-table with especified Liters', async() => { await page.waitToClick(selectors.ticketFuture.tableButtonSearch); - await page.write(selectors.ticketFuture.tableLiters, "0"); - await page.keyboard.press("Enter"); + await page.write(selectors.ticketFuture.tableLiters, '0'); + await page.keyboard.press('Enter'); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitToClick(selectors.ticketFuture.tableButtonSearch); @@ -244,8 +242,8 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitToClick(selectors.ticketFuture.tableButtonSearch); - await page.write(selectors.ticketFuture.tableLiters, "28"); - await page.keyboard.press("Enter"); + await page.write(selectors.ticketFuture.tableLiters, '28'); + await page.keyboard.press('Enter'); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitToClick(selectors.ticketFuture.tableButtonSearch); @@ -254,13 +252,13 @@ describe('Ticket Future path', () => { await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); }); - it('should check the three last tickets and move to the future', async () => { + it('should check the three last tickets and move to the future', async() => { await page.waitToClick(selectors.ticketFuture.multiCheck); await page.waitToClick(selectors.ticketFuture.firstCheck); await page.waitToClick(selectors.ticketFuture.moveButton); await page.waitToClick(selectors.ticketFuture.acceptButton); const message = await page.waitForSnackbar(); + expect(message.text).toContain('Tickets moved successfully!'); }); - }); diff --git a/front/core/lib/component.js b/front/core/lib/component.js index f17db68a2..5695d9449 100644 --- a/front/core/lib/component.js +++ b/front/core/lib/component.js @@ -12,9 +12,10 @@ export default class Component extends EventEmitter { * @param {HTMLElement} $element The main component element * @param {$rootScope.Scope} $scope The element scope * @param {Function} $transclude The transclusion function + * @param {Function} $location The location function */ - constructor($element, $scope, $transclude) { - super(); + constructor($element, $scope, $transclude, $location) { + super($element, $scope, $transclude, $location); this.$ = $scope; if (!$element) return; @@ -164,7 +165,7 @@ export default class Component extends EventEmitter { $transclude.$$boundTransclude.$$slots[slot]; } } -Component.$inject = ['$element', '$scope']; +Component.$inject = ['$element', '$scope', '$location', '$state']; /* * Automatically adds the most used services to the prototype, so they are diff --git a/front/core/services/auth.js b/front/core/services/auth.js index a1dcfa395..04520cd0b 100644 --- a/front/core/services/auth.js +++ b/front/core/services/auth.js @@ -23,7 +23,10 @@ export default class Auth { initialize() { let criteria = { - to: state => state.name != 'login' + to: state => { + const outLayout = ['login', 'recoverPassword', 'resetPassword']; + return !outLayout.some(ol => ol == state.name); + } }; this.$transitions.onStart(criteria, transition => { if (this.loggedIn) diff --git a/front/salix/components/app/app.html b/front/salix/components/app/app.html index d32c9f68b..f14fab2dd 100644 --- a/front/salix/components/app/app.html +++ b/front/salix/components/app/app.html @@ -1,9 +1,8 @@ - - + diff --git a/front/salix/components/app/app.js b/front/salix/components/app/app.js index 1f8cdb46e..20f0ad969 100644 --- a/front/salix/components/app/app.js +++ b/front/salix/components/app/app.js @@ -9,13 +9,20 @@ import Component from 'core/lib/component'; * @property {SideMenu} rightMenu The left menu, if it's present */ export default class App extends Component { + constructor($element, $, $location, $state) { + super($element, $, $location, $state); + this.$location = $location; + this.$state = $state; + } + $postLink() { this.vnApp.logger = this; } get showLayout() { - let state = this.$state.current.name; - return state && state != 'login'; + const state = this.$state.current.name || this.$location.$$path.substring(1).replace('/', '.'); + const outLayout = ['login', 'recoverPassword', 'resetPassword']; + return state && !outLayout.some(ol => ol == state); } $onDestroy() { diff --git a/front/salix/components/index.js b/front/salix/components/index.js index ce4ad585a..dbe9fe81a 100644 --- a/front/salix/components/index.js +++ b/front/salix/components/index.js @@ -5,7 +5,10 @@ import './descriptor-popover'; import './home/home'; import './layout'; import './left-menu/left-menu'; +import './login/index'; import './login/login'; +import './login/recover-password'; +import './login/reset-password'; import './module-card'; import './module-main'; import './side-menu/side-menu'; diff --git a/front/salix/components/log/index.js b/front/salix/components/log/index.js index 2796931a0..f30878b9f 100644 --- a/front/salix/components/log/index.js +++ b/front/salix/components/log/index.js @@ -37,7 +37,7 @@ export default class Controller extends Section { const validations = window.validations; value.forEach(log => { - const locale = validations[log.changedModel].locale ? validations[log.changedModel].locale : {}; + const locale = validations[log.changedModel] && validations[log.changedModel].locale ? validations[log.changedModel].locale : {}; log.oldProperties = this.getInstance(log.oldInstance, locale); log.newProperties = this.getInstance(log.newInstance, locale); diff --git a/front/salix/components/login/index.html b/front/salix/components/login/index.html new file mode 100644 index 000000000..186979f8c --- /dev/null +++ b/front/salix/components/login/index.html @@ -0,0 +1,6 @@ +
+ +
+ +
+
diff --git a/front/salix/components/login/index.js b/front/salix/components/login/index.js new file mode 100644 index 000000000..f0e21fa29 --- /dev/null +++ b/front/salix/components/login/index.js @@ -0,0 +1,16 @@ +import ngModule from '../../module'; +import Component from 'core/lib/component'; +import './style.scss'; + +export default class OutLayout extends Component { + constructor($element, $scope) { + super($element, $scope); + } +} + +OutLayout.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnOutLayout', { + template: require('./index.html'), + controller: OutLayout +}); diff --git a/front/salix/components/login/locale/en.yml b/front/salix/components/login/locale/en.yml index c59a6dd8e..1ddd454b7 100644 --- a/front/salix/components/login/locale/en.yml +++ b/front/salix/components/login/locale/en.yml @@ -1,4 +1,8 @@ User: User Password: Password Do not close session: Do not close session -Enter: Enter \ No newline at end of file +Enter: Enter +Password requirements: > + The password must have at least {{ length }} length characters, + {{nAlpha}} alphabetic characters, {{nUpper}} capital letters, {{nDigits}} + digits and {{nPunct}} symbols (Ex: $%&.) diff --git a/front/salix/components/login/locale/es.yml b/front/salix/components/login/locale/es.yml index 9c9ba5905..e3a5815c1 100644 --- a/front/salix/components/login/locale/es.yml +++ b/front/salix/components/login/locale/es.yml @@ -1,4 +1,16 @@ User: Usuario Password: Contraseña +Email: Correo electrónico Do not close session: No cerrar sesión -Enter: Entrar \ No newline at end of file +Enter: Entrar +I do not remember my password: No recuerdo mi contraseña +Recover password: Recuperar contraseña +We will sent you an email to recover your password: Te enviaremos un correo para restablecer tu contraseña +Notification sent!: ¡Notificación enviada! +Reset password: Restrablecer contraseña +New password: Nueva contraseña +Repeat password: Repetir contraseña +Password requirements: > + La contraseña debe tener al menos {{ length }} caracteres de longitud, + {{nAlpha}} caracteres alfabéticos, {{nUpper}} letras mayúsculas, {{nDigits}} + dígitos y {{nPunct}} símbolos (Ej: $%&.) diff --git a/front/salix/components/login/login.html b/front/salix/components/login/login.html index b15714a23..a078fa0af 100644 --- a/front/salix/components/login/login.html +++ b/front/salix/components/login/login.html @@ -1,27 +1,27 @@ -
- -
- - - - - - - -
+ + + + + + + diff --git a/front/salix/components/login/recover-password.html b/front/salix/components/login/recover-password.html new file mode 100644 index 000000000..73f5401d9 --- /dev/null +++ b/front/salix/components/login/recover-password.html @@ -0,0 +1,17 @@ +
Recover password
+ + +
+ We will sent you an email to recover your password +
+ diff --git a/front/salix/components/login/recover-password.js b/front/salix/components/login/recover-password.js new file mode 100644 index 000000000..fa9bfc459 --- /dev/null +++ b/front/salix/components/login/recover-password.js @@ -0,0 +1,37 @@ +import ngModule from '../../module'; +import './style.scss'; + +export default class Controller { + constructor($scope, $element, $http, vnApp, $translate, $state) { + Object.assign(this, { + $scope, + $element, + $http, + vnApp, + $translate, + $state + }); + } + + goToLogin() { + this.vnApp.showSuccess(this.$translate.instant('Notification sent!')); + this.$state.go('login'); + } + + submit() { + const params = { + email: this.email + }; + + this.$http.post('Accounts/recoverPassword', params) + .then(() => { + this.goToLogin(); + }); + } +} +Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state']; + +ngModule.vnComponent('vnRecoverPassword', { + template: require('./recover-password.html'), + controller: Controller +}); diff --git a/front/salix/components/login/reset-password.html b/front/salix/components/login/reset-password.html new file mode 100644 index 000000000..bdbdc113e --- /dev/null +++ b/front/salix/components/login/reset-password.html @@ -0,0 +1,19 @@ +
Reset password
+ + + + + diff --git a/front/salix/components/login/reset-password.js b/front/salix/components/login/reset-password.js new file mode 100644 index 000000000..9ee1fdb62 --- /dev/null +++ b/front/salix/components/login/reset-password.js @@ -0,0 +1,48 @@ +import ngModule from '../../module'; +import './style.scss'; + +export default class Controller { + constructor($scope, $element, $http, vnApp, $translate, $state, $location) { + Object.assign(this, { + $scope, + $element, + $http, + vnApp, + $translate, + $state, + $location + }); + } + + $onInit() { + this.$http.get('UserPasswords/findOne') + .then(res => { + this.passRequirements = res.data; + }); + } + + submit() { + if (!this.newPassword) + throw new UserError(`You must enter a new password`); + if (this.newPassword != this.repeatPassword) + throw new UserError(`Passwords don't match`); + + const headers = { + Authorization: this.$location.$$search.access_token + }; + + const newPassword = this.newPassword; + + this.$http.post('users/reset-password', {newPassword}, {headers}) + .then(() => { + this.vnApp.showSuccess(this.$translate.instant('Password changed!')); + this.$state.go('login'); + }); + } +} +Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state', '$location']; + +ngModule.vnComponent('vnResetPassword', { + template: require('./reset-password.html'), + controller: Controller +}); diff --git a/front/salix/components/login/style.scss b/front/salix/components/login/style.scss index 8ebf2a68c..8985893f2 100644 --- a/front/salix/components/login/style.scss +++ b/front/salix/components/login/style.scss @@ -1,6 +1,31 @@ @import "variables"; -vn-login { +vn-login, +vn-reset-password, +vn-recover-password{ + .footer { + margin-top: 32px; + text-align: center; + position: relative; + & > .vn-submit { + display: block; + + & > input { + display: block; + width: 100%; + } + } + & > .spinner-wrapper { + position: absolute; + width: 0; + top: 3px; + right: -8px; + overflow: visible; + } + } +} + +vn-out-layout{ position: absolute; height: 100%; width: 100%; @@ -39,28 +64,17 @@ vn-login { white-space: inherit; } } - & > .footer { - margin-top: 32px; - text-align: center; - position: relative; - - & > vn-submit { - display: block; - - & > input { - display: block; - width: 100%; - } - } - & > .spinner-wrapper { - position: absolute; - width: 0; - top: 3px; - right: -8px; - overflow: visible; - } - } } + + h5{ + color: $color-primary; + } + + .text-secondary{ + text-align: center; + padding-bottom: 16px; + } + } @media screen and (max-width: 600px) { @@ -71,4 +85,8 @@ vn-login { box-shadow: none; } } + + a{ + color: $color-primary; + } } diff --git a/front/salix/module.js b/front/salix/module.js index a8de61ae0..01df01a67 100644 --- a/front/salix/module.js +++ b/front/salix/module.js @@ -112,7 +112,7 @@ function $exceptionHandler(vnApp, $window, $state, $injector) { switch (exception.status) { case 401: - if ($state.current.name != 'login') { + if (!$state.current.name.includes('login')) { messageT = 'Session has expired'; let params = {continue: $window.location.hash}; $state.go('login', params); diff --git a/front/salix/routes.js b/front/salix/routes.js index 600907ff1..be893800f 100644 --- a/front/salix/routes.js +++ b/front/salix/routes.js @@ -9,9 +9,17 @@ function config($stateProvider, $urlRouterProvider) { .state('login', { url: '/login?continue', description: 'Login', - views: { - login: {template: ''} - } + template: '' + }) + .state('recoverPassword', { + url: '/recover-password', + description: 'Recover-password', + template: 'asd' + }) + .state('resetPassword', { + url: '/reset-password', + description: 'Reset-password', + template: '' }) .state('home', { url: '/', diff --git a/loopback/locale/en.json b/loopback/locale/en.json index c26941a20..cbff8d75f 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -131,12 +131,15 @@ "Fichadas impares": "Odd signs", "Descanso diario 9h.": "Daily rest 9h.", "Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.", + "Verify email": "Verify email", + "Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Click on the following link to verify this email. If you haven't requested this email, just ignore it", "Password does not meet requirements": "Password does not meet requirements", "You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies", "Not enough privileges to edit a client": "Not enough privileges to edit a client", "Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*", "You don't have grant privilege": "You don't have grant privilege", "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user", + "Email verify": "Email verify", "Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})", "Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production", "App locked": "App locked by user {{userId}}", diff --git a/loopback/locale/es.json b/loopback/locale/es.json index b5ae7bd49..6bf4d79e3 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -246,6 +246,8 @@ "There aren't records for this week": "No existen registros para esta semana", "Empty data source": "Origen de datos vacio", "App locked": "Aplicación bloqueada por el usuario {{userId}}", + "Email verify": "Correo de verificación", + "Landing cannot be lesser than shipment": "Landing cannot be lesser than shipment", "Receipt's bank was not found": "No se encontró el banco del recibo", "This receipt was not compensated": "Este recibo no ha sido compensado", "Client's email was not found": "No se encontró el email del cliente" diff --git a/loopback/server/datasources.json b/loopback/server/datasources.json index 4db642058..00f6bf624 100644 --- a/loopback/server/datasources.json +++ b/loopback/server/datasources.json @@ -113,4 +113,4 @@ "application/x-7z-compressed" ] } -} \ No newline at end of file +} diff --git a/modules/account/back/models/user-password.json b/modules/account/back/models/user-password.json index 1b7e49edd..53909ad1f 100644 --- a/modules/account/back/models/user-password.json +++ b/modules/account/back/models/user-password.json @@ -30,5 +30,13 @@ "type": "number", "required": true } - } + }, + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] } diff --git a/modules/account/front/basic-data/index.js b/modules/account/front/basic-data/index.js index 342297e45..77d3eab26 100644 --- a/modules/account/front/basic-data/index.js +++ b/modules/account/front/basic-data/index.js @@ -2,6 +2,11 @@ import ngModule from '../module'; import Section from 'salix/components/section'; export default class Controller extends Section { + $onInit() { + if (this.$params.emailConfirmed) + this.vnApp.showSuccess(this.$t('Email verified successfully!')); + } + onSubmit() { this.$.watcher.submit() .then(() => this.card.reload()); diff --git a/modules/account/front/basic-data/locale/es.yml b/modules/account/front/basic-data/locale/es.yml new file mode 100644 index 000000000..2ca7bf698 --- /dev/null +++ b/modules/account/front/basic-data/locale/es.yml @@ -0,0 +1 @@ +Email verified successfully!: Correo verificado correctamente! diff --git a/modules/account/front/routes.json b/modules/account/front/routes.json index b96c931c9..a6f2f5d3f 100644 --- a/modules/account/front/routes.json +++ b/modules/account/front/routes.json @@ -74,7 +74,7 @@ } }, { - "url": "/basic-data", + "url": "/basic-data?emailConfirmed", "state": "account.card.basicData", "component": "vn-user-basic-data", "description": "Basic data", diff --git a/modules/client/back/methods/client/filter.js b/modules/client/back/methods/client/filter.js index 3e1ea43bb..1ae569fd3 100644 --- a/modules/client/back/methods/client/filter.js +++ b/modules/client/back/methods/client/filter.js @@ -91,7 +91,18 @@ module.exports = Self => { case 'search': return /^\d+$/.test(value) ? {'c.id': {inq: value}} - : {'c.name': {like: `%${value}%`}}; + : {or: [ + {'c.name': {like: `%${value}%`}}, + {'c.socialName': {like: `%${value}%`}}, + ]}; + case 'phone': + return {or: [ + {'c.phone': {like: `%${value}%`}}, + {'c.mobile': {like: `%${value}%`}}, + ]}; + case 'zoneFk': + param = 'a.postalCode'; + return {[param]: {inq: postalCode}}; case 'name': case 'salesPersonFk': case 'fi': @@ -100,12 +111,8 @@ module.exports = Self => { case 'postcode': case 'provinceFk': case 'email': - case 'phone': param = `c.${param}`; - return {[param]: value}; - case 'zoneFk': - param = 'a.postalCode'; - return {[param]: {inq: postalCode}}; + return {[param]: {like: `%${value}%`}}; } }); @@ -119,6 +126,7 @@ module.exports = Self => { c.fi, c.socialName, c.phone, + c.mobile, c.city, c.postcode, c.email, @@ -132,7 +140,7 @@ module.exports = Self => { LEFT JOIN account.user u ON u.id = c.salesPersonFk LEFT JOIN province p ON p.id = c.provinceFk JOIN vn.address a ON a.clientFk = c.id - ` + ` ); stmt.merge(conn.makeWhere(filter.where)); diff --git a/modules/client/back/methods/client/specs/updatePortfolio.spec.js b/modules/client/back/methods/client/specs/updatePortfolio.spec.js deleted file mode 100644 index bf681eb2e..000000000 --- a/modules/client/back/methods/client/specs/updatePortfolio.spec.js +++ /dev/null @@ -1,75 +0,0 @@ -const models = require('vn-loopback/server/server').models; -const LoopBackContext = require('loopback-context'); - -describe('Client updatePortfolio', () => { - const activeCtx = { - accessToken: {userId: 9}, - http: { - req: { - headers: {origin: 'http://localhost'}, - [`__`]: value => { - return value; - } - } - } - }; - - beforeAll(() => { - spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ - active: activeCtx - }); - }); - - it('should update the portfolioWeight when the salesPerson of a client changes', async() => { - const clientId = 1108; - const salesPersonId = 18; - - const tx = await models.Client.beginTransaction({}); - - try { - const options = {transaction: tx}; - const expectedResult = 841.63; - - const client = await models.Client.findById(clientId, null, options); - await client.updateAttribute('salesPersonFk', salesPersonId, options); - - await models.Client.updatePortfolio(options); - - const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `; - const [salesPerson] = await models.Client.rawSql(portfolioQuery, null, options); - - expect(salesPerson.portfolioWeight).toEqual(expectedResult); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); - - it('should keep the same portfolioWeight when a salesperson is unassigned of a client', async() => { - const clientId = 1107; - const salesPersonId = 19; - const tx = await models.Client.beginTransaction({}); - - try { - const options = {transaction: tx}; - const expectedResult = 34.40; - - const client = await models.Client.findById(clientId, null, options); - await client.updateAttribute('salesPersonFk', null, options); - - await models.Client.updatePortfolio(); - - const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `; - const [salesPerson] = await models.Client.rawSql(portfolioQuery); - - expect(salesPerson.portfolioWeight).toEqual(expectedResult); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); -}); diff --git a/modules/client/back/methods/client/updatePortfolio.js b/modules/client/back/methods/client/updatePortfolio.js deleted file mode 100644 index 809a84636..000000000 --- a/modules/client/back/methods/client/updatePortfolio.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = function(Self) { - Self.remoteMethodCtx('updatePortfolio', { - description: 'Update salesPeson potfolio weight', - accessType: 'READ', - accepts: [], - returns: { - type: 'Object', - root: true - }, - http: { - path: `/updatePortfolio`, - verb: 'GET' - } - }); - - Self.updatePortfolio = async options => { - const myOptions = {}; - - if (typeof options == 'object') - Object.assign(myOptions, options); - - query = `CALL bs.salesPerson_updatePortfolio()`; - return Self.rawSql(query, null, myOptions); - }; -}; diff --git a/modules/client/back/models/client-methods.js b/modules/client/back/models/client-methods.js index 5134e3942..4b20a822c 100644 --- a/modules/client/back/models/client-methods.js +++ b/modules/client/back/models/client-methods.js @@ -22,7 +22,6 @@ module.exports = Self => { require('../methods/client/summary')(Self); require('../methods/client/updateAddress')(Self); require('../methods/client/updateFiscalData')(Self); - require('../methods/client/updatePortfolio')(Self); require('../methods/client/updateUser')(Self); require('../methods/client/uploadFile')(Self); require('../methods/client/campaignMetricsPdf')(Self); diff --git a/modules/client/front/basic-data/index.js b/modules/client/front/basic-data/index.js index 418663952..b08d642d1 100644 --- a/modules/client/front/basic-data/index.js +++ b/modules/client/front/basic-data/index.js @@ -10,8 +10,6 @@ export default class Controller extends Section { onSubmit() { return this.$.watcher.submit().then(() => { - const query = `Clients/updatePortfolio`; - this.$http.get(query); this.$http.get(`Clients/${this.$params.id}/checkDuplicatedData`); }); } diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js index 803338ef3..ee3310368 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js @@ -11,6 +11,7 @@ describe('InvoiceOut createPdf()', () => { const ctx = {req: activeCtx}; it('should create a new PDF file and set true the hasPdf property', async() => { + pending('https://redmine.verdnatura.es/issues/4875'); const invoiceId = 1; spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ active: activeCtx diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js index 08f049783..536fa07a0 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js @@ -30,6 +30,7 @@ describe('InvoiceOut downloadZip()', () => { }); it('should return an error if the size of the files is too large', async() => { + pending('https://redmine.verdnatura.es/issues/4875'); const tx = await models.InvoiceOut.beginTransaction({}); let error; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js index 02f982011..7b5886236 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js @@ -51,6 +51,7 @@ describe('InvoiceOut filter()', () => { }); it('should return the invoice out matching hasPdf', async() => { + pending('https://redmine.verdnatura.es/issues/4875'); const tx = await models.InvoiceOut.beginTransaction({}); const options = {transaction: tx}; diff --git a/modules/route/back/methods/route/getTickets.js b/modules/route/back/methods/route/getTickets.js index 18e5abaf2..708644c1a 100644 --- a/modules/route/back/methods/route/getTickets.js +++ b/modules/route/back/methods/route/getTickets.js @@ -33,36 +33,36 @@ module.exports = Self => { const stmt = new ParameterizedSQL( `SELECT - t.id, - t.packages, - t.warehouseFk, - t.nickname, - t.clientFk, - t.priority, - t.addressFk, - st.code AS ticketStateCode, - st.name AS ticketStateName, - wh.name AS warehouseName, - tob.description AS ticketObservation, - a.street, - a.postalCode, - a.city, - am.name AS agencyModeName, - u.nickname AS userNickname, - vn.ticketTotalVolume(t.id) AS volume, - tob.description - FROM route r - JOIN ticket t ON t.routeFk = r.id - LEFT JOIN ticketState ts ON ts.ticketFk = t.id - LEFT JOIN state st ON st.id = ts.stateFk - LEFT JOIN warehouse wh ON wh.id = t.warehouseFk - LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id - LEFT JOIN observationType ot ON tob.observationTypeFk = ot.id - AND ot.code = 'delivery' - LEFT JOIN address a ON a.id = t.addressFk - LEFT JOIN agencyMode am ON am.id = t.agencyModeFk - LEFT JOIN account.user u ON u.id = r.workerFk - LEFT JOIN vehicle v ON v.id = r.vehicleFk` + t.id, + t.packages, + t.warehouseFk, + t.nickname, + t.clientFk, + t.priority, + t.addressFk, + st.code AS ticketStateCode, + st.name AS ticketStateName, + wh.name AS warehouseName, + tob.description AS ticketObservation, + a.street, + a.postalCode, + a.city, + am.name AS agencyModeName, + u.nickname AS userNickname, + vn.ticketTotalVolume(t.id) AS volume, + tob.description + FROM vn.route r + JOIN ticket t ON t.routeFk = r.id + LEFT JOIN ticketState ts ON ts.ticketFk = t.id + LEFT JOIN state st ON st.id = ts.stateFk + LEFT JOIN warehouse wh ON wh.id = t.warehouseFk + LEFT JOIN observationType ot ON ot.code = 'delivery' + LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id + AND tob.observationTypeFk = ot.id + LEFT JOIN address a ON a.id = t.addressFk + LEFT JOIN agencyMode am ON am.id = t.agencyModeFk + LEFT JOIN account.user u ON u.id = r.workerFk + LEFT JOIN vehicle v ON v.id = r.vehicleFk` ); if (!filter.where) filter.where = {}; diff --git a/modules/worker/back/methods/worker-time-control-mail/checkInbox.js b/modules/worker/back/methods/worker-time-control-mail/checkInbox.js new file mode 100644 index 000000000..7825f38b8 --- /dev/null +++ b/modules/worker/back/methods/worker-time-control-mail/checkInbox.js @@ -0,0 +1,181 @@ +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', + reason: 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 index 2f9559b3a..b38405c1d 100644 --- a/modules/worker/back/methods/worker-time-control/sendMail.js +++ b/modules/worker/back/methods/worker-time-control/sendMail.js @@ -133,7 +133,7 @@ module.exports = Self => { tb.permissionRate, d.isTeleworking FROM tmp.timeBusinessCalculate tb - JOIN user u ON u.id = tb.userFk + JOIN account.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 @@ -143,7 +143,7 @@ module.exports = Self => { 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 + LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk AND tc.dated = tb.dated GROUP BY tb.userFk HAVING isTeleworkingWeek > 0 @@ -332,18 +332,9 @@ module.exports = Self => { }, 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); + const url = `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}`; - query = `INSERT IGNORE INTO workerTimeControlMail (workerFk, year, week) - VALUES (?, ?, ?);`; - await Self.rawSql(query, [previousWorkerFk, args.year, args.week], myOptions); + await models.WorkerTimeControl.weeklyHourRecordEmail(ctx, previousReceiver, args.week, args.year, url); previousWorkerFk = day.workerFk; previousReceiver = day.receiver; 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 index 4cc6e54e3..24bfd6904 100644 --- a/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js +++ b/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js @@ -2,15 +2,12 @@ const models = require('vn-loopback/server/server').models; describe('workerTimeControl sendMail()', () => { const workerId = 18; - const ctx = { - req: { - __: value => { - return value; - } - }, - args: {} - + const activeCtx = { + getLocale: () => { + return 'en'; + } }; + const ctx = {req: activeCtx, args: {}}; it('should fill time control of a worker without records in Journey and with rest', async() => { const tx = await models.WorkerTimeControl.beginTransaction({}); diff --git a/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js b/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js new file mode 100644 index 000000000..0cf614e57 --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js @@ -0,0 +1,53 @@ +const {Email} = require('vn-print'); + +module.exports = Self => { + Self.remoteMethodCtx('weeklyHourRecordEmail', { + description: 'Sends the buyer waste email', + accessType: 'WRITE', + accepts: [ + { + arg: 'recipient', + type: 'string', + description: 'The recipient email', + required: true, + }, + { + arg: 'week', + type: 'number', + required: true, + }, + { + arg: 'year', + type: 'number', + required: true + }, + { + arg: 'url', + type: 'string', + required: true + } + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: '/weekly-hour-hecord-email', + verb: 'POST' + } + }); + + Self.weeklyHourRecordEmail = async(ctx, recipient, week, year, url) => { + const params = { + recipient: recipient, + lang: ctx.req.getLocale(), + week: week, + year: year, + url: url + }; + + const email = new Email('weekly-hour-record', params); + + return email.send(); + }; +}; diff --git a/modules/worker/back/models/worker-time-control-mail.js b/modules/worker/back/models/worker-time-control-mail.js new file mode 100644 index 000000000..36f3851b6 --- /dev/null +++ b/modules/worker/back/models/worker-time-control-mail.js @@ -0,0 +1,3 @@ +module.exports = Self => { + require('../methods/worker-time-control-mail/checkInbox')(Self); +}; diff --git a/modules/worker/back/models/worker-time-control.js b/modules/worker/back/models/worker-time-control.js index 9f802511a..7339f5d15 100644 --- a/modules/worker/back/models/worker-time-control.js +++ b/modules/worker/back/models/worker-time-control.js @@ -7,6 +7,7 @@ module.exports = Self => { require('../methods/worker-time-control/updateTimeEntry')(Self); require('../methods/worker-time-control/sendMail')(Self); require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self); + require('../methods/worker-time-control/weeklyHourRecordEmail')(Self); Self.rewriteDbError(function(err) { if (err.code === 'ER_DUP_ENTRY') diff --git a/package-lock.json b/package-lock.json index 07b0a95fd..2ed6cb275 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "jsdom": "^16.7.0", "jszip": "^3.10.0", "ldapjs": "^2.2.0", - "loopback": "^3.26.0", + "loopback": "^3.28.0", "loopback-boot": "3.3.1", "loopback-component-explorer": "^6.5.0", "loopback-component-storage": "3.6.1", @@ -4878,13 +4878,20 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001299", + "version": "1.0.30001434", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", + "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", "dev": true, - "license": "CC-BY-4.0", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] }, "node_modules/canonical-json": { "version": "0.0.4", @@ -12881,6 +12888,66 @@ "xmlcreate": "^1.0.1" } }, + "node_modules/jsbarcode": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz", + "integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==", + "bin": { + "auto.js": "bin/barcodes/CODE128/auto.js", + "Barcode.js": "bin/barcodes/Barcode.js", + "barcodes": "bin/barcodes", + "canvas.js": "bin/renderers/canvas.js", + "checksums.js": "bin/barcodes/MSI/checksums.js", + "codabar": "bin/barcodes/codabar", + "CODE128": "bin/barcodes/CODE128", + "CODE128_AUTO.js": "bin/barcodes/CODE128/CODE128_AUTO.js", + "CODE128.js": "bin/barcodes/CODE128/CODE128.js", + "CODE128A.js": "bin/barcodes/CODE128/CODE128A.js", + "CODE128B.js": "bin/barcodes/CODE128/CODE128B.js", + "CODE128C.js": "bin/barcodes/CODE128/CODE128C.js", + "CODE39": "bin/barcodes/CODE39", + "constants.js": "bin/barcodes/ITF/constants.js", + "defaults.js": "bin/options/defaults.js", + "EAN_UPC": "bin/barcodes/EAN_UPC", + "EAN.js": "bin/barcodes/EAN_UPC/EAN.js", + "EAN13.js": "bin/barcodes/EAN_UPC/EAN13.js", + "EAN2.js": "bin/barcodes/EAN_UPC/EAN2.js", + "EAN5.js": "bin/barcodes/EAN_UPC/EAN5.js", + "EAN8.js": "bin/barcodes/EAN_UPC/EAN8.js", + "encoder.js": "bin/barcodes/EAN_UPC/encoder.js", + "ErrorHandler.js": "bin/exceptions/ErrorHandler.js", + "exceptions": "bin/exceptions", + "exceptions.js": "bin/exceptions/exceptions.js", + "fixOptions.js": "bin/help/fixOptions.js", + "GenericBarcode": "bin/barcodes/GenericBarcode", + "getOptionsFromElement.js": "bin/help/getOptionsFromElement.js", + "getRenderProperties.js": "bin/help/getRenderProperties.js", + "help": "bin/help", + "index.js": "bin/renderers/index.js", + "index.tmp.js": "bin/barcodes/index.tmp.js", + "ITF": "bin/barcodes/ITF", + "ITF.js": "bin/barcodes/ITF/ITF.js", + "ITF14.js": "bin/barcodes/ITF/ITF14.js", + "JsBarcode.js": "bin/JsBarcode.js", + "linearizeEncodings.js": "bin/help/linearizeEncodings.js", + "merge.js": "bin/help/merge.js", + "MSI": "bin/barcodes/MSI", + "MSI.js": "bin/barcodes/MSI/MSI.js", + "MSI10.js": "bin/barcodes/MSI/MSI10.js", + "MSI1010.js": "bin/barcodes/MSI/MSI1010.js", + "MSI11.js": "bin/barcodes/MSI/MSI11.js", + "MSI1110.js": "bin/barcodes/MSI/MSI1110.js", + "object.js": "bin/renderers/object.js", + "options": "bin/options", + "optionsFromStrings.js": "bin/help/optionsFromStrings.js", + "pharmacode": "bin/barcodes/pharmacode", + "renderers": "bin/renderers", + "shared.js": "bin/renderers/shared.js", + "svg.js": "bin/renderers/svg.js", + "UPC.js": "bin/barcodes/EAN_UPC/UPC.js", + "UPCE.js": "bin/barcodes/EAN_UPC/UPCE.js" + } + }, "node_modules/jsbn": { "version": "0.1.1", "license": "MIT" @@ -23759,6 +23826,14 @@ "version": "1.0.2", "license": "Apache-2.0" }, + "node_modules/xmldom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", + "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "license": "MIT", @@ -23938,6 +24013,7 @@ "fs-extra": "^7.0.1", "intl": "^1.2.5", "js-yaml": "^3.13.1", + "jsbarcode": "^3.11.5", "jsonexport": "^3.2.0", "juice": "^5.2.0", "log4js": "^6.7.0", @@ -23948,7 +24024,8 @@ "strftime": "^0.10.0", "vue": "^2.6.10", "vue-i18n": "^8.15.0", - "vue-server-renderer": "^2.6.10" + "vue-server-renderer": "^2.6.10", + "xmldom": "^0.6.0" } }, "print/node_modules/fs-extra": { @@ -28107,7 +28184,9 @@ "version": "1.0.0" }, "caniuse-lite": { - "version": "1.0.30001299", + "version": "1.0.30001434", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", + "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", "dev": true }, "canonical-json": { @@ -39828,6 +39907,11 @@ "xmlcreate": "^1.0.1" } }, + "jsbarcode": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz", + "integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==" + }, "jsbn": { "version": "0.1.1" }, @@ -57805,6 +57889,7 @@ "fs-extra": "^7.0.1", "intl": "^1.2.5", "js-yaml": "^3.13.1", + "jsbarcode": "^3.11.5", "jsonexport": "^3.2.0", "juice": "^5.2.0", "log4js": "^6.7.0", @@ -57815,7 +57900,8 @@ "strftime": "^0.10.0", "vue": "^2.6.10", "vue-i18n": "^8.15.0", - "vue-server-renderer": "^2.6.10" + "vue-server-renderer": "^2.6.10", + "xmldom": "^0.6.0" }, "dependencies": { "fs-extra": { @@ -59678,6 +59764,11 @@ "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", "integrity": "sha512-Mbe56Dvj00onbnSo9J0qj/XlY5bfN9KidsOnpd5tRCsR3ekB3hyyNU9fGrTdqNT5ZNvv4BsA2TcQlignsZyVcw==" }, + "xmldom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", + "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" + }, "xtend": { <<<<<<< HEAD "version": "1.0.3", diff --git a/print/common/css/misc.css b/print/common/css/misc.css index df8bf571a..ce6c641a0 100644 --- a/print/common/css/misc.css +++ b/print/common/css/misc.css @@ -49,4 +49,10 @@ .page-break-after { page-break-after: always; +} + +.ellipsize { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } \ No newline at end of file diff --git a/print/core/smtp.js b/print/core/smtp.js index a55ba448d..61b115b5a 100644 --- a/print/core/smtp.js +++ b/print/core/smtp.js @@ -8,10 +8,12 @@ module.exports = { this.transporter = nodemailer.createTransport(config.smtp); }, - send(options) { + async send(options) { options.from = `${config.app.senderName} <${config.app.senderEmail}>`; if (process.env.NODE_ENV !== 'production') { + const notProductionError = {message: 'This not production, this email not sended'}; + await this.mailLog(options, notProductionError); if (!config.smtp.auth.user) return Promise.resolve(true); @@ -24,29 +26,35 @@ module.exports = { throw err; }).finally(async() => { - const attachments = []; - if (options.attachments) { - for (let attachment of options.attachments) { - const fileName = attachment.filename; - const filePath = attachment.path; - if (fileName.includes('.png')) continue; - - if (fileName || filePath) - attachments.push(filePath ? filePath : fileName); - } - } - - const fileNames = attachments.join(',\n'); - await db.rawSql(` - INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status) - VALUES (?, ?, 1, ?, ?, ?, ?)`, [ - options.to, - options.replyTo, - options.subject, - options.text || options.html, - fileNames, - error && error.message || 'Sent' - ]); + await this.mailLog(options, error); }); + }, + + async mailLog(options, error) { + const attachments = []; + if (options.attachments) { + for (let attachment of options.attachments) { + const fileName = attachment.filename; + const filePath = attachment.path; + if (fileName.includes('.png')) continue; + + if (fileName || filePath) + attachments.push(filePath ? filePath : fileName); + } + } + + const fileNames = attachments.join(',\n'); + + await db.rawSql(` + INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status) + VALUES (?, ?, 1, ?, ?, ?, ?)`, [ + options.to, + options.replyTo, + options.subject, + options.text || options.html, + fileNames, + error && error.message || 'Sent' + ]); } + }; diff --git a/print/templates/email/email-verify/assets/css/import.js b/print/templates/email/email-verify/assets/css/import.js new file mode 100644 index 000000000..7360587f7 --- /dev/null +++ b/print/templates/email/email-verify/assets/css/import.js @@ -0,0 +1,13 @@ +const Stylesheet = require(`vn-print/core/stylesheet`); + +const path = require('path'); +const vnPrintPath = path.resolve('print'); + +module.exports = new Stylesheet([ + `${vnPrintPath}/common/css/spacing.css`, + `${vnPrintPath}/common/css/misc.css`, + `${vnPrintPath}/common/css/layout.css`, + `${vnPrintPath}/common/css/email.css`, + `${__dirname}/style.css`]) + .mergeStyles(); + diff --git a/print/templates/email/email-verify/assets/css/style.css b/print/templates/email/email-verify/assets/css/style.css new file mode 100644 index 000000000..5db85befa --- /dev/null +++ b/print/templates/email/email-verify/assets/css/style.css @@ -0,0 +1,5 @@ +.external-link { + border: 2px dashed #8dba25; + border-radius: 3px; + text-align: center +} \ No newline at end of file diff --git a/print/templates/email/email-verify/email-verify.html b/print/templates/email/email-verify/email-verify.html new file mode 100644 index 000000000..b8a6e263c --- /dev/null +++ b/print/templates/email/email-verify/email-verify.html @@ -0,0 +1,48 @@ + + + + + + {{ $t('subject') }} + + + + + + + + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+

+ {{ $t(`click`) }} + {{ $t('subject') }} +

+
+
+ + +
+
+ +
+
+ +
+
+
+
+ + diff --git a/print/templates/email/email-verify/email-verify.js b/print/templates/email/email-verify/email-verify.js new file mode 100755 index 000000000..7f0b80a13 --- /dev/null +++ b/print/templates/email/email-verify/email-verify.js @@ -0,0 +1,17 @@ +const Component = require(`vn-print/core/component`); +const emailHeader = new Component('email-header'); +const emailFooter = new Component('email-footer'); + +module.exports = { + name: 'email-verify', + components: { + 'email-header': emailHeader.build(), + 'email-footer': emailFooter.build() + }, + props: { + url: { + type: [String], + required: true + } + } +}; diff --git a/print/templates/email/email-verify/locale/en.yml b/print/templates/email/email-verify/locale/en.yml new file mode 100644 index 000000000..0298f53b4 --- /dev/null +++ b/print/templates/email/email-verify/locale/en.yml @@ -0,0 +1,3 @@ +subject: Email Verify +title: Email Verify +click: Click on the following link to verify this email. If you haven't requested this email, just ignore it diff --git a/print/templates/email/email-verify/locale/es.yml b/print/templates/email/email-verify/locale/es.yml new file mode 100644 index 000000000..37bd6ef27 --- /dev/null +++ b/print/templates/email/email-verify/locale/es.yml @@ -0,0 +1,3 @@ +subject: Verificar correo +title: Verificar correo +click: Pulsa en el siguiente link para verificar este correo. Si no has pedido este correo, simplemente ignóralo diff --git a/print/templates/email/recover-password/assets/css/import.js b/print/templates/email/recover-password/assets/css/import.js new file mode 100644 index 000000000..7360587f7 --- /dev/null +++ b/print/templates/email/recover-password/assets/css/import.js @@ -0,0 +1,13 @@ +const Stylesheet = require(`vn-print/core/stylesheet`); + +const path = require('path'); +const vnPrintPath = path.resolve('print'); + +module.exports = new Stylesheet([ + `${vnPrintPath}/common/css/spacing.css`, + `${vnPrintPath}/common/css/misc.css`, + `${vnPrintPath}/common/css/layout.css`, + `${vnPrintPath}/common/css/email.css`, + `${__dirname}/style.css`]) + .mergeStyles(); + diff --git a/print/templates/email/recover-password/assets/css/style.css b/print/templates/email/recover-password/assets/css/style.css new file mode 100644 index 000000000..5db85befa --- /dev/null +++ b/print/templates/email/recover-password/assets/css/style.css @@ -0,0 +1,5 @@ +.external-link { + border: 2px dashed #8dba25; + border-radius: 3px; + text-align: center +} \ No newline at end of file diff --git a/print/templates/email/recover-password/locale/es.yml b/print/templates/email/recover-password/locale/es.yml new file mode 100644 index 000000000..c72b108ee --- /dev/null +++ b/print/templates/email/recover-password/locale/es.yml @@ -0,0 +1,3 @@ +subject: Recuperar contraseña +title: Recuperar contraseña +Click on the following link to change your password.: Pulsa en el siguiente link para cambiar tu contraseña. diff --git a/print/templates/email/recover-password/recover-password.html b/print/templates/email/recover-password/recover-password.html new file mode 100644 index 000000000..a654b3d5f --- /dev/null +++ b/print/templates/email/recover-password/recover-password.html @@ -0,0 +1,48 @@ + + + + + + {{ $t('subject') }} + + + + + + + + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+

+ {{ $t('Click on the following link to change your password.') }} + {{ $t('subject') }} +

+
+
+ + +
+
+ +
+
+ +
+
+
+
+ + diff --git a/print/templates/email/recover-password/recover-password.js b/print/templates/email/recover-password/recover-password.js new file mode 100755 index 000000000..b589411a9 --- /dev/null +++ b/print/templates/email/recover-password/recover-password.js @@ -0,0 +1,17 @@ +const Component = require(`vn-print/core/component`); +const emailHeader = new Component('email-header'); +const emailFooter = new Component('email-footer'); + +module.exports = { + name: 'recover-password', + components: { + 'email-header': emailHeader.build(), + 'email-footer': emailFooter.build() + }, + props: { + url: { + type: [String], + required: true + } + } +}; diff --git a/print/templates/email/weekly-hour-record/assets/css/import.js b/print/templates/email/weekly-hour-record/assets/css/import.js new file mode 100644 index 000000000..1582b82c5 --- /dev/null +++ b/print/templates/email/weekly-hour-record/assets/css/import.js @@ -0,0 +1,12 @@ +const Stylesheet = require(`vn-print/core/stylesheet`); + +const path = require('path'); +const vnPrintPath = path.resolve('print'); + +module.exports = new Stylesheet([ + `${vnPrintPath}/common/css/spacing.css`, + `${vnPrintPath}/common/css/misc.css`, + `${vnPrintPath}/common/css/layout.css`, + `${vnPrintPath}/common/css/email.css`]) + .mergeStyles(); + diff --git a/print/templates/email/weekly-hour-record/locale/en.yml b/print/templates/email/weekly-hour-record/locale/en.yml new file mode 100644 index 000000000..817e5451e --- /dev/null +++ b/print/templates/email/weekly-hour-record/locale/en.yml @@ -0,0 +1,6 @@ +subject: Weekly time log +title: Record of hours week {0} year {1} +dear: Dear worker +description: Access the following link:

+ {0}

+ Click 'SATISFIED' if you agree with the hours worked. Otherwise, press 'NOT SATISFIED', detailing the cause of the disagreement. diff --git a/print/templates/email/weekly-hour-record/locale/es.yml b/print/templates/email/weekly-hour-record/locale/es.yml new file mode 100644 index 000000000..b70862f16 --- /dev/null +++ b/print/templates/email/weekly-hour-record/locale/es.yml @@ -0,0 +1,6 @@ +subject: Registro de horas semanal +title: Registro de horas semana {0} año {1} +dear: Estimado trabajador +description: Acceda al siguiente enlace:

+ {0}

+ Pulse 'CONFORME' si esta de acuerdo con las horas trabajadas. En caso contrario pulse 'NO CONFORME', detallando la causa de la disconformidad. diff --git a/print/templates/email/weekly-hour-record/weekly-hour-record.html b/print/templates/email/weekly-hour-record/weekly-hour-record.html new file mode 100644 index 000000000..84abb4c61 --- /dev/null +++ b/print/templates/email/weekly-hour-record/weekly-hour-record.html @@ -0,0 +1,9 @@ + +
+
+

{{ $t('title', [week, year]) }}

+

{{$t('dear')}},

+

+
+
+
diff --git a/print/templates/email/weekly-hour-record/weekly-hour-record.js b/print/templates/email/weekly-hour-record/weekly-hour-record.js new file mode 100755 index 000000000..8fdaea0ce --- /dev/null +++ b/print/templates/email/weekly-hour-record/weekly-hour-record.js @@ -0,0 +1,23 @@ +const Component = require(`vn-print/core/component`); +const emailBody = new Component('email-body'); + +module.exports = { + name: 'weekly-hour-record', + components: { + 'email-body': emailBody.build() + }, + props: { + week: { + type: Number, + required: true + }, + year: { + type: Number, + required: true + }, + url: { + type: String, + required: true + } + } +}; diff --git a/print/templates/reports/collection-label/assets/css/style.css b/print/templates/reports/collection-label/assets/css/style.css index fe1975445..597921c92 100644 --- a/print/templates/reports/collection-label/assets/css/style.css +++ b/print/templates/reports/collection-label/assets/css/style.css @@ -1,6 +1,6 @@ html { font-family: "Roboto"; - margin-top: -7px; + margin-top: -6px; } * { box-sizing: border-box; @@ -9,7 +9,7 @@ html { } #vertical { writing-mode: vertical-rl; - height: 226px; + height: 240px; margin-left: -13px; } .outline { @@ -18,6 +18,7 @@ html { } #nickname { font-size: 22px; + max-width: 50px; } #agencyDescripton { font-size: 32px; diff --git a/print/templates/reports/collection-label/collection-label.html b/print/templates/reports/collection-label/collection-label.html index eeb82ac4a..6716d1fe5 100644 --- a/print/templates/reports/collection-label/collection-label.html +++ b/print/templates/reports/collection-label/collection-label.html @@ -1,35 +1,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{labelData.levelV}}{{labelData.ticketFk}} ⬸ {{labelData.clientFk}}{{labelData.shipped}}
{{labelData.workerCode}}
{{labelData.labelCount}}
{{labelData.size}}
{{labelData.agencyDescription}}
{{labelData.lineCount}}
{{labelData.nickName}}{{labelData.agencyHour}}
- -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{labelData.collectionFk ? `${labelData.collectionFk} ~ ${labelData.wagon}-${labelData.level}` : '-'.repeat(23)}} + + {{labelData.clientFk ? `${labelData.ticketFk} « ${labelData.clientFk}` : labelData.ticketFk}} + {{labelData.shipped ? labelData.shipped : '---'}}
{{labelData.workerCode ? labelData.workerCode : '---'}}
{{labelData.labelCount ? labelData.labelCount : 0}}
{{labelData.code == 'plant' ? labelData.size + 'cm' : labelData.volume + 'm³'}}
{{labelData.agencyDescription}}
{{labelData.lineCount ? labelData.lineCount : 0}}
{{labelData.nickName ? labelData.nickName : '---'}}{{labelData.shipped ? labelData.shippedHour : labelData.zoneHour}}
+ + \ No newline at end of file diff --git a/print/templates/reports/collection-label/collection-label.js b/print/templates/reports/collection-label/collection-label.js index 7bc25ad79..d2d5f6417 100644 --- a/print/templates/reports/collection-label/collection-label.js +++ b/print/templates/reports/collection-label/collection-label.js @@ -40,7 +40,7 @@ module.exports = { format: 'code128', displayValue: false, width: 3.8, - height: 110, + height: 115, }); return xmlSerializer.serializeToString(svgNode); }, diff --git a/print/templates/reports/collection-label/options.json b/print/templates/reports/collection-label/options.json index 175b3c1db..ae88e6c0c 100644 --- a/print/templates/reports/collection-label/options.json +++ b/print/templates/reports/collection-label/options.json @@ -2,8 +2,8 @@ "width": "10.4cm", "height": "4.8cm", "margin": { - "top": "0cm", - "right": "0.5cm", + "top": "0.3cm", + "right": "0.6cm", "bottom": "0cm", "left": "0cm" }, diff --git a/print/templates/reports/collection-label/sql/labelsData.sql b/print/templates/reports/collection-label/sql/labelsData.sql index 6f5b47a54..b799b289b 100644 --- a/print/templates/reports/collection-label/sql/labelsData.sql +++ b/print/templates/reports/collection-label/sql/labelsData.sql @@ -1,21 +1,23 @@ -SELECT c.itemPackingTypeFk, - CONCAT(tc.collectionFk, ' ', LEFT(cc.code, 4)) color, - CONCAT(tc.collectionFk, ' ', SUBSTRING('ABCDEFGH',tc.wagon, 1), '-', tc.`level`) levelV, - tc.ticketFk, - LEFT(COALESCE(et.description, zo.name, am.name),12) agencyDescription, +SELECT tc.collectionFk, + SUBSTRING('ABCDEFGH', tc.wagon, 1) wagon, + tc.`level`, + t.id ticketFk, + COALESCE(et.description, zo.name, am.name) agencyDescription, am.name, t.clientFk, - CONCAT(CAST(SUM(sv.volume) AS DECIMAL(5, 2)), 'm³') m3 , - CAST(IF(ic.code = 'plant', CONCAT(MAX(i.`size`),' cm'), COUNT(*)) AS CHAR) size, + CAST(SUM(sv.volume) AS DECIMAL(5, 2)) volume, + MAX(i.`size`) `size`, + ic.code, w.code workerCode, - tt.labelCount, - IF(HOUR(t.shipped), TIME_FORMAT(t.shipped, '%H:%i'), TIME_FORMAT(zo.`hour`, '%H:%i')) agencyHour, + TIME_FORMAT(t.shipped, '%H:%i') shippedHour, + TIME_FORMAT(zo.`hour`, '%H:%i') zoneHour, DATE_FORMAT(t.shipped, '%d/%m/%y') shipped, - COUNT(*) lineCount, - t.nickName + t.nickName, + tt.labelCount, + COUNT(*) lineCount FROM vn.ticket t - JOIN vn.ticketCollection tc ON tc.ticketFk = t.id - JOIN vn.collection c ON c.id = tc.collectionFk + LEFT JOIN vn.ticketCollection tc ON tc.ticketFk = t.id + LEFT JOIN vn.collection c ON c.id = tc.collectionFk LEFT JOIN vn.collectionColors cc ON cc.shelve = tc.`level` AND cc.wagon = tc.wagon AND cc.trainFk = c.trainFk @@ -24,12 +26,12 @@ SELECT c.itemPackingTypeFk, JOIN vn.item i ON i.id = s.itemFk JOIN vn.itemType it ON it.id = i.typeFk JOIN vn.itemCategory ic ON ic.id = it.categoryFk - JOIN vn.worker w ON w.id = c.workerFk + LEFT JOIN vn.worker w ON w.id = c.workerFk JOIN vn.agencyMode am ON am.id = t.agencyModeFk LEFT JOIN vn.ticketTrolley tt ON tt.ticket = t.id LEFT JOIN vn.`zone` zo ON t.zoneFk = zo.id LEFT JOIN vn.routesMonitor rm ON rm.routeFk = t.routeFk LEFT JOIN vn.expeditionTruck et ON et.id = rm.expeditionTruckFk - WHERE tc.ticketFk IN (?) + WHERE t.id IN (?) GROUP BY t.id ORDER BY cc.`code`; \ No newline at end of file