Merge branch 'dev' into 4732-previa-label
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Pau 2022-12-02 08:05:04 +00:00
commit 5235ed5499
83 changed files with 1277 additions and 362 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

@ -32,7 +32,7 @@ module.exports = Self => {
let message = $t(`There's a new urgent ticket:`); let message = $t(`There's a new urgent ticket:`);
const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id='; const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
tickets.forEach(ticket => { 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({ const department = await models.Department.findOne({
@ -42,7 +42,5 @@ module.exports = Self => {
if (channelName) if (channelName)
return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`); return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`);
return;
}; };
}; };

View File

@ -27,7 +27,7 @@ describe('Chat notifyIssue()', () => {
subject: 'Issue title'} subject: 'Issue title'}
]); ]);
// eslint-disable-next-line max-len // 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); const department = await app.models.Department.findById(departmentId);
let orgChatName = department.chatName; let orgChatName = department.chatName;

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

@ -4,4 +4,23 @@ module.exports = Self => {
require('../methods/chat/sendCheckingPresence')(Self); require('../methods/chat/sendCheckingPresence')(Self);
require('../methods/chat/notifyIssues')(Self); require('../methods/chat/notifyIssues')(Self);
require('../methods/chat/sendQueued')(Self); require('../methods/chat/sendQueued')(Self);
Self.observe('before save', async function(ctx) {
if (!ctx.isNewInstance) return;
let {message} = ctx.instance;
if (!message) return;
const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g);
if (!parts) return;
const replacedParts = parts.map(part => {
return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, '');
});
for (const [index, part] of parts.entries())
message = message.replace(part, replacedParts[index]);
ctx.instance.message = message;
});
}; };

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

@ -57,14 +57,16 @@ module.exports = Self => {
r.clientFk, r.clientFk,
FALSE hasPdf, FALSE hasPdf,
FALSE isInvoice, FALSE isInvoice,
CASE WHEN at2.code LIKE 'compensation' THEN True ELSE False END as isCompensation at2.id IS NOT NULL as isCompensation
FROM vn.receipt r FROM vn.receipt r
LEFT JOIN vn.worker w ON w.id = r.workerFk LEFT JOIN vn.worker w ON w.id = r.workerFk
LEFT JOIN account.user u ON u.id = w.userFk LEFT JOIN account.user u ON u.id = w.userFk
JOIN vn.company c ON c.id = r.companyFk JOIN vn.company c ON c.id = r.companyFk
JOIN vn.accounting a ON a.id = r.bankFk LEFT JOIN vn.accounting a ON a.id = r.bankFk
JOIN vn.accountingType at2 ON at2.id = a.accountingTypeFk LEFT JOIN vn.accountingType at2 ON at2.id = a.accountingTypeFk AND at2.code = 'compensation'
WHERE r.clientFk = ? AND r.companyFk = ? WHERE
r.clientFk = ?
AND r.companyFk = ?
UNION ALL UNION ALL
SELECT SELECT
i.id, i.id,
@ -81,13 +83,10 @@ module.exports = Self => {
i.clientFk, i.clientFk,
i.hasPdf, i.hasPdf,
TRUE isInvoice, TRUE isInvoice,
CASE WHEN at2.code LIKE 'compensation' THEN True ELSE False END as isCompensation NULL
FROM vn.invoiceOut i FROM vn.invoiceOut i
JOIN vn.company c ON c.id = i.companyFk JOIN vn.company c ON c.id = i.companyFk
JOIN vn.accounting a ON a.id = i.bankFk
JOIN vn.accountingType at2 ON at2.id = a.accountingTypeFk
WHERE i.clientFk = ? AND i.companyFk = ? WHERE i.clientFk = ? AND i.companyFk = ?
ORDER BY payed DESC, created DESC
) t ORDER BY payed DESC, created DESC`, ) t ORDER BY payed DESC, created DESC`,
[ [
clientId, clientId,

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

@ -11,6 +11,7 @@ describe('InvoiceOut createPdf()', () => {
const ctx = {req: activeCtx}; const ctx = {req: activeCtx};
it('should create a new PDF file and set true the hasPdf property', async() => { it('should create a new PDF file and set true the hasPdf property', async() => {
pending('https://redmine.verdnatura.es/issues/4875');
const invoiceId = 1; const invoiceId = 1;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx

View File

@ -30,6 +30,7 @@ describe('InvoiceOut downloadZip()', () => {
}); });
it('should return an error if the size of the files is too large', async() => { 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({}); const tx = await models.InvoiceOut.beginTransaction({});
let error; let error;

View File

@ -51,6 +51,7 @@ describe('InvoiceOut filter()', () => {
}); });
it('should return the invoice out matching hasPdf', async() => { it('should return the invoice out matching hasPdf', async() => {
pending('https://redmine.verdnatura.es/issues/4875');
const tx = await models.InvoiceOut.beginTransaction({}); const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};

View File

@ -137,9 +137,11 @@ module.exports = Self => {
const params = [args.id, args.shipped, args.warehouseFk]; const params = [args.id, args.shipped, args.warehouseFk];
const [salesMovable] = await Self.rawSql(query, params, myOptions); const [salesMovable] = await Self.rawSql(query, params, myOptions);
const sales = await models.Sale.find({ticketFk: args.id}, myOptions);
const salesNewTicket = salesMovable.filter(sale => (sale.movable ? sale.movable : 0) >= sale.quantity); const salesNewTicket = salesMovable.filter(sale => (sale.movable ? sale.movable : 0) >= sale.quantity);
if (salesNewTicket.length) { const salesNewTicketLength = salesNewTicket.length;
if (salesNewTicketLength && sales.length != salesNewTicketLength) {
const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions); const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions);
args.id = newTicket.id; args.id = newTicket.id;
} }

View File

@ -67,6 +67,7 @@ class Controller extends Component {
ticketHaveNegatives() { ticketHaveNegatives() {
let haveNegatives = false; let haveNegatives = false;
let haveNotNegatives = false; let haveNotNegatives = false;
this.ticket.withoutNegatives = false;
const haveDifferences = this.ticket.sale.haveDifferences; const haveDifferences = this.ticket.sale.haveDifferences;
this.ticket.sale.items.forEach(item => { this.ticket.sale.items.forEach(item => {
@ -76,8 +77,9 @@ class Controller extends Component {
haveNotNegatives = true; haveNotNegatives = true;
}); });
this.ticket.withoutNegatives = true;
this.haveNegatives = (haveNegatives && haveNotNegatives && haveDifferences); this.haveNegatives = (haveNegatives && haveNotNegatives && haveDifferences);
if (this.haveNegatives)
this.ticket.withoutNegatives = true;
} }
onSubmit() { onSubmit() {

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": {
@ -27170,7 +27247,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": {
@ -32604,6 +32683,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"
}, },
@ -39035,6 +39119,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",
@ -39045,7 +39130,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": {
@ -40382,6 +40468,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": {
"version": "4.0.2" "version": "4.0.2"
}, },

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: Arial, Helvetica, sans-serif;
margin-top: -7px; margin-top: -6px;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
@ -9,29 +9,36 @@ html {
} }
#vertical { #vertical {
writing-mode: vertical-rl; writing-mode: vertical-rl;
height: 226px; height: 230px;
font-size: 29px;
margin-left: -13px; margin-left: -13px;
} }
.outline { .outline {
border: 1px solid black; border: 1px solid black;
padding: 5px; padding: 5px;
height: 37px;
width: 100px;
} }
#nickname { #nickname {
font-size: 22px; font-size: 22px;
max-width: 50px;
} }
#agencyDescripton { #agencyDescripton {
font-size: 32px; font-size: 32px;
width: 375px;
font-weight: bold; font-weight: bold;
} }
#bold { #bold {
font-weight: bold; font-weight: bold;
} }
#barcode{ #barcode{
width: 390px; width: 370px;
} }
#shipped { #shipped {
font-weight: bold; font-weight: bold;
width: 50px;
max-width: 100px;
} }
#ticketFk, #vertical { #ticketFk {
font-size: 34px; font-size: 32px;
} }

View File

@ -1,35 +1,34 @@
<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">{{getVertical(labelData)}}</span></td>
<td rowspan="6"><span id="vertical">{{labelData.levelV}}</span></td> <td id="ticketFk">
<td id="ticketFk">{{labelData.ticketFk}} ⬸ {{labelData.clientFk}}</td> {{labelData.clientFk ? `${labelData.ticketFk} « ${labelData.clientFk}` : labelData.ticketFk}}
<td colspan="2" id="shipped">{{labelData.shipped}}</td> </td>
</tr> <td colspan="2" id="shipped">{{labelData.shipped || '---'}}</td>
<tr> </tr>
<td rowspan="3"><div v-html="getBarcode(labelData.ticketFk)" id="barcode"></div></td> <tr>
<td class="outline">{{labelData.workerCode}}</td> <td rowspan="3"><div v-html="getBarcode(labelData.ticketFk)" id="barcode"></div></td>
</tr> <td class="outline">{{labelData.workerCode || '---'}}</td>
<tr> </tr>
<td class="outline">{{labelData.labelCount}}</td> <tr>
</tr> <td class="outline">{{labelData.labelCount || 0}}</td>
<tr> </tr>
<td class="outline">{{labelData.size}}</td> <tr>
</tr> <td class="outline">{{labelData.code == 'V' ? (labelData.size || 0) + 'cm' : (labelData.volume || 0) + 'm³'}}</td>
<tr> </tr>
<td><div id="agencyDescripton">{{labelData.agencyDescription}}</div></td> <tr>
<td id="bold">{{labelData.lineCount}}</td> <td><div id="agencyDescripton" class="ellipsize">{{labelData.agencyDescription ? labelData.agencyDescription.toUpperCase() : '---'}}</div></td>
</tr> <td id="bold">{{labelData.lineCount || 0}}</td>
<tr> </tr>
<td id="nickname">{{labelData.nickName}}</td> <tr>
<td id="bold">{{labelData.agencyHour}}</td> <td id="nickname" class="ellipsize">{{labelData.nickName ? labelData.nickName.toUpperCase() : '---'}}</td>
</tr> <td id="bold">{{labelData.shippedHour || labelData.zoneHour}}</td>
</tbody> </tr>
</table> </tbody>
<template v-slot:footer> </table>
<span></span> </body>
</template> </html>
</report-body>

View File

@ -25,7 +25,6 @@ module.exports = {
ticketIds = [this.id]; ticketIds = [this.id];
this.labelsData = await this.rawSqlFromDef('labelsData', [ticketIds]); this.labelsData = await this.rawSqlFromDef('labelsData', [ticketIds]);
if (!this.labelsData.length) if (!this.labelsData.length)
throw new UserError('Empty data source'); throw new UserError('Empty data source');
}, },
@ -40,10 +39,23 @@ 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);
}, },
getVertical(labelData) {
let value;
if (labelData.collectionFk) {
value = `${labelData.collectionFk} ~ `;
if (labelData.code == 'V')
value = value + `${labelData.wagon}-${labelData.level}`;
else
value = value + `${labelData.color.substring(0, 4)}`;
} else
value = '-'.repeat(19);
return value;
},
}, },
components: { components: {
'report-body': reportBody.build() 'report-body': reportBody.build()

View File

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

View File

@ -1,18 +1,20 @@
SELECT c.itemPackingTypeFk, SELECT c.itemPackingTypeFk code,
CONCAT(tc.collectionFk, ' ', LEFT(cc.code, 4)) color, tc.collectionFk,
CONCAT(tc.collectionFk, ' ', SUBSTRING('ABCDEFGH',tc.wagon, 1), '-', tc.`level`) levelV, SUBSTRING('ABCDEFGH', tc.wagon, 1) wagon,
tc.ticketFk, tc.`level`,
LEFT(COALESCE(et.description, zo.name, am.name),12) agencyDescription, t.id ticketFk,
am.name, COALESCE(et.description, zo.name, am.name) agencyDescription,
cc.code color,
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`,
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 JOIN vn.ticketCollection tc ON tc.ticketFk = t.id
JOIN vn.collection c ON c.id = tc.collectionFk JOIN vn.collection c ON c.id = tc.collectionFk
@ -30,6 +32,6 @@ SELECT c.itemPackingTypeFk,
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`;