Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4846-claim.search-panel
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Vicent Llopis 2022-11-30 10:53:54 +01:00
commit 68855add59
75 changed files with 1250 additions and 374 deletions

View File

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

View File

@ -1,6 +1,6 @@
const app = require('vn-loopback/server/server'); 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() => { it('should throw an error when password does not meet requirements', async() => {
let req = app.models.Account.setPassword(1, 'insecurePass'); let req = app.models.Account.setPassword(1, 'insecurePass');

View File

@ -51,7 +51,7 @@ module.exports = Self => {
const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file); const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file);
await fs.unlink(dstFile); await fs.unlink(dstFile);
} catch (err) { } catch (err) {
if (err.code != 'ENOENT') if (err.code != 'ENOENT' && dms.file)
throw err; throw err;
} }

View File

@ -1,4 +1,7 @@
/* eslint max-len: ["error", { "code": 150 }]*/
const md5 = require('md5'); const md5 = require('md5');
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
module.exports = Self => { module.exports = Self => {
require('../methods/account/login')(Self); require('../methods/account/login')(Self);
@ -6,6 +9,7 @@ module.exports = Self => {
require('../methods/account/acl')(Self); require('../methods/account/acl')(Self);
require('../methods/account/change-password')(Self); require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self); require('../methods/account/set-password')(Self);
require('../methods/account/recover-password')(Self);
require('../methods/account/validate-token')(Self); require('../methods/account/validate-token')(Self);
require('../methods/account/privileges')(Self); require('../methods/account/privileges')(Self);
@ -27,17 +31,62 @@ module.exports = Self => {
ctx.data.password = md5(ctx.data.password); 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', { Self.remoteMethod('getCurrentUserData', {
description: 'Gets the current user data', description: 'Gets the current user data',
accepts: [ accepts: [
{ {
arg: 'ctx', arg: 'ctx',
type: 'Object', type: 'object',
http: {source: 'context'} http: {source: 'context'}
} }
], ],
returns: { returns: {
type: 'Object', type: 'object',
root: true root: true
}, },
http: { http: {
@ -58,7 +107,7 @@ module.exports = Self => {
* *
* @param {Integer} userId The user id * @param {Integer} userId The user id
* @param {String} name The role name * @param {String} name The role name
* @param {Object} options Options * @param {object} options Options
* @return {Boolean} %true if user has the role, %false otherwise * @return {Boolean} %true if user has the role, %false otherwise
*/ */
Self.hasRole = async function(userId, name, options) { Self.hasRole = async function(userId, name, options) {
@ -70,8 +119,8 @@ module.exports = Self => {
* Get all user roles. * Get all user roles.
* *
* @param {Integer} userId The user id * @param {Integer} userId The user id
* @param {Object} options Options * @param {object} options Options
* @return {Object} User role list * @return {object} User role list
*/ */
Self.getRoles = async(userId, options) => { Self.getRoles = async(userId, options) => {
let result = await Self.rawSql( let result = await Self.rawSql(

View File

@ -40,6 +40,9 @@
"email": { "email": {
"type": "string" "type": "string"
}, },
"emailVerified": {
"type": "boolean"
},
"created": { "created": {
"type": "date" "type": "date"
}, },
@ -88,16 +91,23 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
}, },
{
"property": "recoverPassword",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{ {
"property": "logout", "property": "logout",
"accessType": "EXECUTE", "accessType": "EXECUTE",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$authenticated", "principalId": "$authenticated",
"permission": "ALLOW" "permission": "ALLOW"
}, },
{ {
"property": "validateToken", "property": "validateToken",
"accessType": "EXECUTE", "accessType": "EXECUTE",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$authenticated", "principalId": "$authenticated",

View File

@ -1,14 +1,14 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('loopback model Account', () => { describe('loopback model Account', () => {
it('should return true if the user has the given role', async() => { 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(); expect(result).toBeTruthy();
}); });
it('should return false if the user doesnt have the given role', async() => { 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(); expect(result).toBeFalsy();
}); });

View File

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

27
back/models/user.js Normal file
View File

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

View File

@ -0,0 +1,2 @@
DELETE FROM `salix`.`ACL`
WHERE model = 'UserPassword';

View File

@ -2564,10 +2564,6 @@ UPDATE `vn`.`route`
UPDATE `vn`.`route` UPDATE `vn`.`route`
SET `invoiceInFk`=2 SET `invoiceInFk`=2
WHERE `id`=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`) INSERT INTO `bs`.`sale` (`saleFk`, `amount`, `dated`, `typeFk`, `clientFk`)
VALUES VALUES

View File

@ -81044,4 +81044,3 @@ USE `vncontrol`;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2022-09-16 10:44:31 -- Dump completed on 2022-09-16 10:44:31

View File

@ -29,6 +29,11 @@ export default {
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]', firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]' 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: { accountIndex: {
addAccount: 'vn-user-index button vn-icon[icon="add"]', addAccount: 'vn-user-index button vn-icon[icon="add"]',
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]', newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',

View File

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

View File

@ -5,80 +5,78 @@ describe('Ticket Future path', () => {
let browser; let browser;
let page; let page;
beforeAll(async () => { beforeAll(async() => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('employee', 'ticket'); await page.loginAndModule('employee', 'ticket');
await page.accessToSection('ticket.future'); await page.accessToSection('ticket.future');
}); });
afterAll(async () => { afterAll(async() => {
await browser.close(); await browser.close();
}); });
const now = new Date(); const now = new Date();
const tomorrow = new Date(now.getDate() + 1); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.warehouseFk); await page.clearInput(selectors.ticketFuture.warehouseFk);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
let message = await page.waitForSnackbar(); let message = await page.waitForSnackbar();
expect(message.text).toContain('warehouseFk is a required argument'); expect(message.text).toContain('warehouseFk is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.litersMax); await page.clearInput(selectors.ticketFuture.litersMax);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('litersMax is a required argument'); expect(message.text).toContain('litersMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.linesMax); await page.clearInput(selectors.ticketFuture.linesMax);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('linesMax is a required argument'); expect(message.text).toContain('linesMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.futureDated); await page.clearInput(selectors.ticketFuture.futureDated);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('futureDated is a required argument'); expect(message.text).toContain('futureDated is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.originDated); await page.clearInput(selectors.ticketFuture.originDated);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('originDated is a required argument'); 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.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, now); await page.pickDate(selectors.ticketFuture.shipped, now);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, tomorrow); await page.pickDate(selectors.ticketFuture.shipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
await page.pickDate(selectors.ticketFuture.tfShipped, now); await page.pickDate(selectors.ticketFuture.tfShipped, now);
@ -86,14 +84,14 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow); await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
@ -108,7 +106,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
@ -123,7 +121,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
@ -138,7 +136,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 3); 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.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped); await page.clearInput(selectors.ticketFuture.shipped);
@ -164,10 +162,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableId, "13"); await page.write(selectors.ticketFuture.tableId, '13');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 2); await page.waitForNumberOfElements(selectors.ticketFuture.table, 2);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -176,10 +174,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableTfId, "12"); await page.write(selectors.ticketFuture.tableTfId, '12');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -188,7 +186,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical'); await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
@ -199,7 +197,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical'); await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
@ -210,10 +208,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLines, "0"); await page.write(selectors.ticketFuture.tableLines, '0');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -222,8 +220,8 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLines, "1"); await page.write(selectors.ticketFuture.tableLines, '1');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -232,10 +230,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLiters, "0"); await page.write(selectors.ticketFuture.tableLiters, '0');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -244,8 +242,8 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLiters, "28"); await page.write(selectors.ticketFuture.tableLiters, '28');
await page.keyboard.press("Enter"); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch); await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -254,13 +252,13 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); 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.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck); await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton); await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.ticketFuture.acceptButton); await page.waitToClick(selectors.ticketFuture.acceptButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!'); expect(message.text).toContain('Tickets moved successfully!');
}); });
}); });

View File

@ -12,9 +12,10 @@ export default class Component extends EventEmitter {
* @param {HTMLElement} $element The main component element * @param {HTMLElement} $element The main component element
* @param {$rootScope.Scope} $scope The element scope * @param {$rootScope.Scope} $scope The element scope
* @param {Function} $transclude The transclusion function * @param {Function} $transclude The transclusion function
* @param {Function} $location The location function
*/ */
constructor($element, $scope, $transclude) { constructor($element, $scope, $transclude, $location) {
super(); super($element, $scope, $transclude, $location);
this.$ = $scope; this.$ = $scope;
if (!$element) return; if (!$element) return;
@ -164,7 +165,7 @@ export default class Component extends EventEmitter {
$transclude.$$boundTransclude.$$slots[slot]; $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 * Automatically adds the most used services to the prototype, so they are

View File

@ -23,7 +23,10 @@ export default class Auth {
initialize() { initialize() {
let criteria = { 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 => { this.$transitions.onStart(criteria, transition => {
if (this.loggedIn) if (this.loggedIn)

View File

@ -1,9 +1,8 @@
<vn-layout <vn-layout
ng-if="$ctrl.showLayout"> ng-if="$ctrl.showLayout">
</vn-layout> </vn-layout>
<ui-view <vn-out-layout
name="login"
ng-if="!$ctrl.showLayout"> ng-if="!$ctrl.showLayout">
</ui-view> </vn-out-layout>
<vn-snackbar vn-id="snackbar"></vn-snackbar> <vn-snackbar vn-id="snackbar"></vn-snackbar>
<vn-debug-info></vn-debug-info> <vn-debug-info></vn-debug-info>

View File

@ -9,13 +9,20 @@ import Component from 'core/lib/component';
* @property {SideMenu} rightMenu The left menu, if it's present * @property {SideMenu} rightMenu The left menu, if it's present
*/ */
export default class App extends Component { export default class App extends Component {
constructor($element, $, $location, $state) {
super($element, $, $location, $state);
this.$location = $location;
this.$state = $state;
}
$postLink() { $postLink() {
this.vnApp.logger = this; this.vnApp.logger = this;
} }
get showLayout() { get showLayout() {
let state = this.$state.current.name; const state = this.$state.current.name || this.$location.$$path.substring(1).replace('/', '.');
return state && state != 'login'; const outLayout = ['login', 'recoverPassword', 'resetPassword'];
return state && !outLayout.some(ol => ol == state);
} }
$onDestroy() { $onDestroy() {

View File

@ -5,7 +5,10 @@ import './descriptor-popover';
import './home/home'; import './home/home';
import './layout'; import './layout';
import './left-menu/left-menu'; import './left-menu/left-menu';
import './login/index';
import './login/login'; import './login/login';
import './login/recover-password';
import './login/reset-password';
import './module-card'; import './module-card';
import './module-main'; import './module-main';
import './side-menu/side-menu'; import './side-menu/side-menu';

View File

@ -37,7 +37,7 @@ export default class Controller extends Section {
const validations = window.validations; const validations = window.validations;
value.forEach(log => { 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.oldProperties = this.getInstance(log.oldInstance, locale);
log.newProperties = this.getInstance(log.newInstance, locale); log.newProperties = this.getInstance(log.newInstance, locale);

View File

@ -0,0 +1,6 @@
<div class="box">
<img src="./logo.svg"/>
<form name="form">
<ui-view></ui-view>
</form>
</div>

View File

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

View File

@ -1,4 +1,8 @@
User: User User: User
Password: Password Password: Password
Do not close session: Do not close session Do not close session: Do not close session
Enter: Enter 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: $%&.)

View File

@ -1,4 +1,16 @@
User: Usuario User: Usuario
Password: Contraseña Password: Contraseña
Email: Correo electrónico
Do not close session: No cerrar sesión Do not close session: No cerrar sesión
Enter: Entrar 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: $%&.)

View File

@ -1,27 +1,27 @@
<div class="box"> <vn-textfield
<img src="./logo.svg"/> label="User"
<form name="form" ng-submit="$ctrl.submit()"> ng-model="$ctrl.user"
<vn-textfield vn-id="userField"
label="User" vn-focus>
ng-model="$ctrl.user" </vn-textfield>
vn-id="userField" <vn-textfield
vn-focus> label="Password"
</vn-textfield> ng-model="$ctrl.password"
<vn-textfield type="password">
label="Password" </vn-textfield>
ng-model="$ctrl.password" <vn-check
type="password"> label="Do not close session"
</vn-textfield> ng-model="$ctrl.remember"
<vn-check name="remember">
label="Do not close session" </vn-check>
ng-model="$ctrl.remember" <div class="footer">
name="remember"> <vn-submit label="Enter" ng-click="$ctrl.submit()"></vn-submit>
</vn-check> <div class="spinner-wrapper">
<div class="footer"> <vn-spinner enable="$ctrl.loading"></vn-spinner>
<vn-submit label="Enter"></vn-submit> </div>
<div class="spinner-wrapper"> <div class="vn-pt-lg">
<vn-spinner enable="$ctrl.loading"></vn-spinner> <a ui-sref="recoverPassword" translate>
</div> I do not remember my password
</div> </a>
</form> </div>
</div> </div>

View File

@ -0,0 +1,17 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Recover password</h5>
<vn-textfield
label="Email"
ng-model="$ctrl.email"
vn-focus>
</vn-textfield>
<div
class="text-secondary"
translate>
We will sent you an email to recover your password
</div>
<div class="footer">
<vn-submit label="Recover password" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>

View File

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

View File

@ -0,0 +1,19 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Reset password</h5>
<vn-textfield
label="New password"
ng-model="$ctrl.newPassword"
type="password"
info="{{'Password requirements' | translate:$ctrl.passRequirements}}"
vn-focus>
</vn-textfield>
<vn-textfield
label="Repeat password"
ng-model="$ctrl.repeatPassword"
type="password">
</vn-textfield>
<div class="footer">
<vn-submit label="Reset password" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>

View File

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

View File

@ -1,6 +1,31 @@
@import "variables"; @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; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -39,28 +64,17 @@ vn-login {
white-space: inherit; 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) { @media screen and (max-width: 600px) {
@ -71,4 +85,8 @@ vn-login {
box-shadow: none; box-shadow: none;
} }
} }
a{
color: $color-primary;
}
} }

View File

@ -112,7 +112,7 @@ function $exceptionHandler(vnApp, $window, $state, $injector) {
switch (exception.status) { switch (exception.status) {
case 401: case 401:
if ($state.current.name != 'login') { if (!$state.current.name.includes('login')) {
messageT = 'Session has expired'; messageT = 'Session has expired';
let params = {continue: $window.location.hash}; let params = {continue: $window.location.hash};
$state.go('login', params); $state.go('login', params);

View File

@ -9,9 +9,17 @@ function config($stateProvider, $urlRouterProvider) {
.state('login', { .state('login', {
url: '/login?continue', url: '/login?continue',
description: 'Login', description: 'Login',
views: { template: '<vn-login></vn-login>'
login: {template: '<vn-login></vn-login>'} })
} .state('recoverPassword', {
url: '/recover-password',
description: 'Recover-password',
template: '<vn-recover-password>asd</vn-recover-password>'
})
.state('resetPassword', {
url: '/reset-password',
description: 'Reset-password',
template: '<vn-reset-password></vn-reset-password>'
}) })
.state('home', { .state('home', {
url: '/', url: '/',

View File

@ -131,12 +131,15 @@
"Fichadas impares": "Odd signs", "Fichadas impares": "Odd signs",
"Descanso diario 9h.": "Daily rest 9h.", "Descanso diario 9h.": "Daily rest 9h.",
"Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.", "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", "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", "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", "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}}*", "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 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", "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}}})", "Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})",
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production", "Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production",
"Receipt's bank was not found": "Receipt's bank was not found", "Receipt's bank was not found": "Receipt's bank was not found",

View File

@ -245,6 +245,8 @@
"Already has this status": "Ya tiene este estado", "Already has this status": "Ya tiene este estado",
"There aren't records for this week": "No existen registros para esta semana", "There aren't records for this week": "No existen registros para esta semana",
"Empty data source": "Origen de datos vacio", "Empty data source": "Origen de datos vacio",
"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", "Receipt's bank was not found": "No se encontró el banco del recibo",
"This receipt was not compensated": "Este recibo no ha sido compensado", "This receipt was not compensated": "Este recibo no ha sido compensado",
"Client's email was not found": "No se encontró el email del cliente" "Client's email was not found": "No se encontró el email del cliente"

View File

@ -113,4 +113,4 @@
"application/x-7z-compressed" "application/x-7z-compressed"
] ]
} }
} }

View File

@ -30,5 +30,13 @@
"type": "number", "type": "number",
"required": true "required": true
} }
} },
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
} }

View File

@ -2,6 +2,11 @@ import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
export default class Controller extends Section { export default class Controller extends Section {
$onInit() {
if (this.$params.emailConfirmed)
this.vnApp.showSuccess(this.$t('Email verified successfully!'));
}
onSubmit() { onSubmit() {
this.$.watcher.submit() this.$.watcher.submit()
.then(() => this.card.reload()); .then(() => this.card.reload());

View File

@ -0,0 +1 @@
Email verified successfully!: Correo verificado correctamente!

View File

@ -74,7 +74,7 @@
} }
}, },
{ {
"url": "/basic-data", "url": "/basic-data?emailConfirmed",
"state": "account.card.basicData", "state": "account.card.basicData",
"component": "vn-user-basic-data", "component": "vn-user-basic-data",
"description": "Basic data", "description": "Basic data",

View File

@ -91,7 +91,18 @@ module.exports = Self => {
case 'search': case 'search':
return /^\d+$/.test(value) return /^\d+$/.test(value)
? {'c.id': {inq: 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 'name':
case 'salesPersonFk': case 'salesPersonFk':
case 'fi': case 'fi':
@ -100,12 +111,8 @@ module.exports = Self => {
case 'postcode': case 'postcode':
case 'provinceFk': case 'provinceFk':
case 'email': case 'email':
case 'phone':
param = `c.${param}`; param = `c.${param}`;
return {[param]: value}; return {[param]: {like: `%${value}%`}};
case 'zoneFk':
param = 'a.postalCode';
return {[param]: {inq: postalCode}};
} }
}); });
@ -119,6 +126,7 @@ module.exports = Self => {
c.fi, c.fi,
c.socialName, c.socialName,
c.phone, c.phone,
c.mobile,
c.city, c.city,
c.postcode, c.postcode,
c.email, c.email,
@ -132,7 +140,7 @@ module.exports = Self => {
LEFT JOIN account.user u ON u.id = c.salesPersonFk LEFT JOIN account.user u ON u.id = c.salesPersonFk
LEFT JOIN province p ON p.id = c.provinceFk LEFT JOIN province p ON p.id = c.provinceFk
JOIN vn.address a ON a.clientFk = c.id JOIN vn.address a ON a.clientFk = c.id
` `
); );
stmt.merge(conn.makeWhere(filter.where)); stmt.merge(conn.makeWhere(filter.where));

View File

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

View File

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

View File

@ -22,7 +22,6 @@ module.exports = Self => {
require('../methods/client/summary')(Self); require('../methods/client/summary')(Self);
require('../methods/client/updateAddress')(Self); require('../methods/client/updateAddress')(Self);
require('../methods/client/updateFiscalData')(Self); require('../methods/client/updateFiscalData')(Self);
require('../methods/client/updatePortfolio')(Self);
require('../methods/client/updateUser')(Self); require('../methods/client/updateUser')(Self);
require('../methods/client/uploadFile')(Self); require('../methods/client/uploadFile')(Self);
require('../methods/client/campaignMetricsPdf')(Self); require('../methods/client/campaignMetricsPdf')(Self);

View File

@ -10,8 +10,6 @@ export default class Controller extends Section {
onSubmit() { onSubmit() {
return this.$.watcher.submit().then(() => { return this.$.watcher.submit().then(() => {
const query = `Clients/updatePortfolio`;
this.$http.get(query);
this.$http.get(`Clients/${this.$params.id}/checkDuplicatedData`); this.$http.get(`Clients/${this.$params.id}/checkDuplicatedData`);
}); });
} }

View File

@ -33,36 +33,36 @@ module.exports = Self => {
const stmt = new ParameterizedSQL( const stmt = new ParameterizedSQL(
`SELECT `SELECT
t.id, t.id,
t.packages, t.packages,
t.warehouseFk, t.warehouseFk,
t.nickname, t.nickname,
t.clientFk, t.clientFk,
t.priority, t.priority,
t.addressFk, t.addressFk,
st.code AS ticketStateCode, st.code AS ticketStateCode,
st.name AS ticketStateName, st.name AS ticketStateName,
wh.name AS warehouseName, wh.name AS warehouseName,
tob.description AS ticketObservation, tob.description AS ticketObservation,
a.street, a.street,
a.postalCode, a.postalCode,
a.city, a.city,
am.name AS agencyModeName, am.name AS agencyModeName,
u.nickname AS userNickname, u.nickname AS userNickname,
vn.ticketTotalVolume(t.id) AS volume, vn.ticketTotalVolume(t.id) AS volume,
tob.description tob.description
FROM route r FROM vn.route r
JOIN ticket t ON t.routeFk = r.id JOIN ticket t ON t.routeFk = r.id
LEFT JOIN ticketState ts ON ts.ticketFk = t.id LEFT JOIN ticketState ts ON ts.ticketFk = t.id
LEFT JOIN state st ON st.id = ts.stateFk LEFT JOIN state st ON st.id = ts.stateFk
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id LEFT JOIN observationType ot ON ot.code = 'delivery'
LEFT JOIN observationType ot ON tob.observationTypeFk = ot.id LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id
AND ot.code = 'delivery' AND tob.observationTypeFk = ot.id
LEFT JOIN address a ON a.id = t.addressFk LEFT JOIN address a ON a.id = t.addressFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN account.user u ON u.id = r.workerFk LEFT JOIN account.user u ON u.id = r.workerFk
LEFT JOIN vehicle v ON v.id = r.vehicleFk` LEFT JOIN vehicle v ON v.id = r.vehicleFk`
); );
if (!filter.where) filter.where = {}; if (!filter.where) filter.where = {};

View File

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

View File

@ -133,7 +133,7 @@ module.exports = Self => {
tb.permissionRate, tb.permissionRate,
d.isTeleworking d.isTeleworking
FROM tmp.timeBusinessCalculate tb 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 department d ON d.id = tb.departmentFk
JOIN business b ON b.id = tb.businessFk 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 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)), IF(tc.timeWorkDecimal > 0, FALSE, IF(tb.timeWorkDecimal > 0, TRUE, FALSE)),
TRUE))isTeleworkingWeek TRUE))isTeleworkingWeek
FROM tmp.timeBusinessCalculate tb 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 AND tc.dated = tb.dated
GROUP BY tb.userFk GROUP BY tb.userFk
HAVING isTeleworkingWeek > 0 HAVING isTeleworkingWeek > 0
@ -332,18 +332,9 @@ module.exports = Self => {
}, myOptions); }, myOptions);
const timestamp = started.getTime() / 1000; const timestamp = started.getTime() / 1000;
await models.Mail.create({ const url = `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}`;
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) await models.WorkerTimeControl.weeklyHourRecordEmail(ctx, previousReceiver, args.week, args.year, url);
VALUES (?, ?, ?);`;
await Self.rawSql(query, [previousWorkerFk, args.year, args.week], myOptions);
previousWorkerFk = day.workerFk; previousWorkerFk = day.workerFk;
previousReceiver = day.receiver; previousReceiver = day.receiver;

View File

@ -2,15 +2,12 @@ const models = require('vn-loopback/server/server').models;
describe('workerTimeControl sendMail()', () => { describe('workerTimeControl sendMail()', () => {
const workerId = 18; const workerId = 18;
const ctx = { const activeCtx = {
req: { getLocale: () => {
__: value => { return 'en';
return value; }
}
},
args: {}
}; };
const ctx = {req: activeCtx, args: {}};
it('should fill time control of a worker without records in Journey and with rest', async() => { it('should fill time control of a worker without records in Journey and with rest', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({}); const tx = await models.WorkerTimeControl.beginTransaction({});

View File

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

View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/worker-time-control-mail/checkInbox')(Self);
};

View File

@ -7,6 +7,7 @@ module.exports = Self => {
require('../methods/worker-time-control/updateTimeEntry')(Self); require('../methods/worker-time-control/updateTimeEntry')(Self);
require('../methods/worker-time-control/sendMail')(Self); require('../methods/worker-time-control/sendMail')(Self);
require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self); require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self);
require('../methods/worker-time-control/weeklyHourRecordEmail')(Self);
Self.rewriteDbError(function(err) { Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY') if (err.code === 'ER_DUP_ENTRY')

111
package-lock.json generated
View File

@ -24,7 +24,7 @@
"jsdom": "^16.7.0", "jsdom": "^16.7.0",
"jszip": "^3.10.0", "jszip": "^3.10.0",
"ldapjs": "^2.2.0", "ldapjs": "^2.2.0",
"loopback": "^3.26.0", "loopback": "^3.28.0",
"loopback-boot": "3.3.1", "loopback-boot": "3.3.1",
"loopback-component-explorer": "^6.5.0", "loopback-component-explorer": "^6.5.0",
"loopback-component-storage": "3.6.1", "loopback-component-storage": "3.6.1",
@ -4878,13 +4878,20 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "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, "dev": true,
"license": "CC-BY-4.0", "funding": [
"funding": { {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/browserslist" "url": "https://opencollective.com/browserslist"
} },
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
}
]
}, },
"node_modules/canonical-json": { "node_modules/canonical-json": {
"version": "0.0.4", "version": "0.0.4",
@ -12881,6 +12888,66 @@
"xmlcreate": "^1.0.1" "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": { "node_modules/jsbn": {
"version": "0.1.1", "version": "0.1.1",
"license": "MIT" "license": "MIT"
@ -23759,6 +23826,14 @@
"version": "1.0.2", "version": "1.0.2",
"license": "Apache-2.0" "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": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"license": "MIT", "license": "MIT",
@ -23938,6 +24013,7 @@
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"intl": "^1.2.5", "intl": "^1.2.5",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"jsbarcode": "^3.11.5",
"jsonexport": "^3.2.0", "jsonexport": "^3.2.0",
"juice": "^5.2.0", "juice": "^5.2.0",
"log4js": "^6.7.0", "log4js": "^6.7.0",
@ -23948,7 +24024,8 @@
"strftime": "^0.10.0", "strftime": "^0.10.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-i18n": "^8.15.0", "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": { "print/node_modules/fs-extra": {
@ -28107,7 +28184,9 @@
"version": "1.0.0" "version": "1.0.0"
}, },
"caniuse-lite": { "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 "dev": true
}, },
"canonical-json": { "canonical-json": {
@ -39828,6 +39907,11 @@
"xmlcreate": "^1.0.1" "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": { "jsbn": {
"version": "0.1.1" "version": "0.1.1"
}, },
@ -57805,6 +57889,7 @@
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"intl": "^1.2.5", "intl": "^1.2.5",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"jsbarcode": "^3.11.5",
"jsonexport": "^3.2.0", "jsonexport": "^3.2.0",
"juice": "^5.2.0", "juice": "^5.2.0",
"log4js": "^6.7.0", "log4js": "^6.7.0",
@ -57815,7 +57900,8 @@
"strftime": "^0.10.0", "strftime": "^0.10.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-i18n": "^8.15.0", "vue-i18n": "^8.15.0",
"vue-server-renderer": "^2.6.10" "vue-server-renderer": "^2.6.10",
"xmldom": "^0.6.0"
}, },
"dependencies": { "dependencies": {
"fs-extra": { "fs-extra": {
@ -59678,6 +59764,11 @@
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz",
"integrity": "sha512-Mbe56Dvj00onbnSo9J0qj/XlY5bfN9KidsOnpd5tRCsR3ekB3hyyNU9fGrTdqNT5ZNvv4BsA2TcQlignsZyVcw==" "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": { "xtend": {
<<<<<<< HEAD <<<<<<< HEAD
"version": "1.0.3", "version": "1.0.3",

View File

@ -49,4 +49,10 @@
.page-break-after { .page-break-after {
page-break-after: always; page-break-after: always;
}
.ellipsize {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }

View File

@ -8,10 +8,12 @@ module.exports = {
this.transporter = nodemailer.createTransport(config.smtp); this.transporter = nodemailer.createTransport(config.smtp);
}, },
send(options) { async send(options) {
options.from = `${config.app.senderName} <${config.app.senderEmail}>`; options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
if (process.env.NODE_ENV !== 'production') { 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) if (!config.smtp.auth.user)
return Promise.resolve(true); return Promise.resolve(true);
@ -24,29 +26,35 @@ module.exports = {
throw err; throw err;
}).finally(async() => { }).finally(async() => {
const attachments = []; await this.mailLog(options, error);
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'
]);
}); });
},
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'
]);
} }
}; };

View File

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

View File

@ -0,0 +1,5 @@
.external-link {
border: 2px dashed #8dba25;
border-radius: 3px;
text-align: center
}

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<p>
{{ $t(`click`) }}
<a :href="url">{{ $t('subject') }}</a>
</p>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
.external-link {
border: 2px dashed #8dba25;
border-radius: 3px;
text-align: center
}

View File

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

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<p>
{{ $t('Click on the following link to change your password.') }}
<a :href="url">{{ $t('subject') }}</a>
</p>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

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

View File

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

View File

@ -0,0 +1,6 @@
subject: Weekly time log
title: Record of hours week {0} year {1}
dear: Dear worker
description: Access the following link:<br/><br/>
{0} <br/><br/>
Click 'SATISFIED' if you agree with the hours worked. Otherwise, press 'NOT SATISFIED', detailing the cause of the disagreement.

View File

@ -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:<br/><br/>
{0} <br/><br/>
Pulse 'CONFORME' si esta de acuerdo con las horas trabajadas. En caso contrario pulse 'NO CONFORME', detallando la causa de la disconformidad.

View File

@ -0,0 +1,9 @@
<email-body v-bind="$props">
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title', [week, year]) }}</h1>
<p>{{$t('dear')}},</p>
<p v-html="$t('description', [url])"></p>
</div>
</div>
</email-body>

View File

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

View File

@ -1,6 +1,6 @@
html { html {
font-family: "Roboto"; font-family: "Roboto";
margin-top: -7px; margin-top: -6px;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
@ -9,7 +9,7 @@ html {
} }
#vertical { #vertical {
writing-mode: vertical-rl; writing-mode: vertical-rl;
height: 226px; height: 240px;
margin-left: -13px; margin-left: -13px;
} }
.outline { .outline {
@ -18,6 +18,7 @@ html {
} }
#nickname { #nickname {
font-size: 22px; font-size: 22px;
max-width: 50px;
} }
#agencyDescripton { #agencyDescripton {
font-size: 32px; font-size: 32px;

View File

@ -1,35 +1,36 @@
<report-body v-bind="$props"> <!DOCTYPE html>
<template v-slot:header> <html>
<span></span> <body>
</template> <table v-for="labelData in labelsData" style="break-before: page">
<table v-for="labelData in labelsData"> <tbody>
<tbody> <tr>
<tr> <td rowspan="6"><span id="vertical" class="ellipsize">
<td rowspan="6"><span id="vertical">{{labelData.levelV}}</span></td> {{labelData.collectionFk ? `${labelData.collectionFk} ~ ${labelData.wagon}-${labelData.level}` : '-'.repeat(23)}}
<td id="ticketFk">{{labelData.ticketFk}} ⬸ {{labelData.clientFk}}</td> </span></td>
<td colspan="2" id="shipped">{{labelData.shipped}}</td> <td id="ticketFk">
</tr> {{labelData.clientFk ? `${labelData.ticketFk} « ${labelData.clientFk}` : labelData.ticketFk}}
<tr> </td>
<td rowspan="3"><div v-html="getBarcode(labelData.ticketFk)" id="barcode"></div></td> <td colspan="2" id="shipped">{{labelData.shipped ? labelData.shipped : '---'}}</td>
<td class="outline">{{labelData.workerCode}}</td> </tr>
</tr> <tr>
<tr> <td rowspan="3"><div v-html="getBarcode(labelData.ticketFk)" id="barcode"></div></td>
<td class="outline">{{labelData.labelCount}}</td> <td class="outline">{{labelData.workerCode ? labelData.workerCode : '---'}}</td>
</tr> </tr>
<tr> <tr>
<td class="outline">{{labelData.size}}</td> <td class="outline">{{labelData.labelCount ? labelData.labelCount : 0}}</td>
</tr> </tr>
<tr> <tr>
<td><div id="agencyDescripton">{{labelData.agencyDescription}}</div></td> <td class="outline">{{labelData.code == 'plant' ? labelData.size + 'cm' : labelData.volume + 'm³'}}</td>
<td id="bold">{{labelData.lineCount}}</td> </tr>
</tr> <tr>
<tr> <td><div id="agencyDescripton" class="ellipsize">{{labelData.agencyDescription}}</div></td>
<td id="nickname">{{labelData.nickName}}</td> <td id="bold">{{labelData.lineCount ? labelData.lineCount : 0}}</td>
<td id="bold">{{labelData.agencyHour}}</td> </tr>
</tr> <tr>
</tbody> <td id="nickname" class="ellipsize">{{labelData.nickName ? labelData.nickName : '---'}}</td>
</table> <td id="bold">{{labelData.shipped ? labelData.shippedHour : labelData.zoneHour}}</td>
<template v-slot:footer> </tr>
<span></span> </tbody>
</template> </table>
</report-body> </body>
</html>

View File

@ -40,7 +40,7 @@ module.exports = {
format: 'code128', format: 'code128',
displayValue: false, displayValue: false,
width: 3.8, width: 3.8,
height: 110, height: 115,
}); });
return xmlSerializer.serializeToString(svgNode); return xmlSerializer.serializeToString(svgNode);
}, },

View File

@ -2,8 +2,8 @@
"width": "10.4cm", "width": "10.4cm",
"height": "4.8cm", "height": "4.8cm",
"margin": { "margin": {
"top": "0cm", "top": "0.3cm",
"right": "0.5cm", "right": "0.6cm",
"bottom": "0cm", "bottom": "0cm",
"left": "0cm" "left": "0cm"
}, },

View File

@ -1,21 +1,23 @@
SELECT c.itemPackingTypeFk, SELECT tc.collectionFk,
CONCAT(tc.collectionFk, ' ', LEFT(cc.code, 4)) color, SUBSTRING('ABCDEFGH', tc.wagon, 1) wagon,
CONCAT(tc.collectionFk, ' ', SUBSTRING('ABCDEFGH',tc.wagon, 1), '-', tc.`level`) levelV, tc.`level`,
tc.ticketFk, t.id ticketFk,
LEFT(COALESCE(et.description, zo.name, am.name),12) agencyDescription, COALESCE(et.description, zo.name, am.name) agencyDescription,
am.name, am.name,
t.clientFk, t.clientFk,
CONCAT(CAST(SUM(sv.volume) AS DECIMAL(5, 2)), '') m3 , CAST(SUM(sv.volume) AS DECIMAL(5, 2)) volume,
CAST(IF(ic.code = 'plant', CONCAT(MAX(i.`size`),' cm'), COUNT(*)) AS CHAR) size, MAX(i.`size`) `size`,
ic.code,
w.code workerCode, w.code workerCode,
tt.labelCount, TIME_FORMAT(t.shipped, '%H:%i') shippedHour,
IF(HOUR(t.shipped), TIME_FORMAT(t.shipped, '%H:%i'), TIME_FORMAT(zo.`hour`, '%H:%i')) agencyHour, TIME_FORMAT(zo.`hour`, '%H:%i') zoneHour,
DATE_FORMAT(t.shipped, '%d/%m/%y') shipped, DATE_FORMAT(t.shipped, '%d/%m/%y') shipped,
COUNT(*) lineCount, t.nickName,
t.nickName tt.labelCount,
COUNT(*) lineCount
FROM vn.ticket t FROM vn.ticket t
JOIN vn.ticketCollection tc ON tc.ticketFk = t.id LEFT JOIN vn.ticketCollection tc ON tc.ticketFk = t.id
JOIN vn.collection c ON c.id = tc.collectionFk LEFT JOIN vn.collection c ON c.id = tc.collectionFk
LEFT JOIN vn.collectionColors cc ON cc.shelve = tc.`level` LEFT JOIN vn.collectionColors cc ON cc.shelve = tc.`level`
AND cc.wagon = tc.wagon AND cc.wagon = tc.wagon
AND cc.trainFk = c.trainFk AND cc.trainFk = c.trainFk
@ -24,12 +26,12 @@ SELECT c.itemPackingTypeFk,
JOIN vn.item i ON i.id = s.itemFk JOIN vn.item i ON i.id = s.itemFk
JOIN vn.itemType it ON it.id = i.typeFk JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.itemCategory ic ON ic.id = it.categoryFk 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 JOIN vn.agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN vn.ticketTrolley tt ON tt.ticket = t.id LEFT JOIN vn.ticketTrolley tt ON tt.ticket = t.id
LEFT JOIN vn.`zone` zo ON t.zoneFk = zo.id LEFT JOIN vn.`zone` zo ON t.zoneFk = zo.id
LEFT JOIN vn.routesMonitor rm ON rm.routeFk = t.routeFk LEFT JOIN vn.routesMonitor rm ON rm.routeFk = t.routeFk
LEFT JOIN vn.expeditionTruck et ON et.id = rm.expeditionTruckFk LEFT JOIN vn.expeditionTruck et ON et.id = rm.expeditionTruckFk
WHERE tc.ticketFk IN (?) WHERE t.id IN (?)
GROUP BY t.id GROUP BY t.id
ORDER BY cc.`code`; ORDER BY cc.`code`;