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');
describe('account changePassword()', () => {
describe('account setPassword()', () => {
it('should throw an error when password does not meet requirements', async() => {
let req = app.models.Account.setPassword(1, 'insecurePass');

View File

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

View File

@ -1,4 +1,7 @@
/* eslint max-len: ["error", { "code": 150 }]*/
const md5 = require('md5');
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
module.exports = Self => {
require('../methods/account/login')(Self);
@ -6,6 +9,7 @@ module.exports = Self => {
require('../methods/account/acl')(Self);
require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self);
require('../methods/account/recover-password')(Self);
require('../methods/account/validate-token')(Self);
require('../methods/account/privileges')(Self);
@ -27,17 +31,62 @@ module.exports = Self => {
ctx.data.password = md5(ctx.data.password);
});
Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
if (!ctx.args || !ctx.args.data.email) return;
const models = Self.app.models;
const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const url = origin.split(':');
const userId = ctx.instance.id;
const user = await models.user.findById(userId);
class Mailer {
async send(verifyOptions, cb) {
const params = {
url: verifyOptions.verifyHref,
recipient: verifyOptions.to,
lang: ctx.req.getLocale()
};
const email = new Email('email-verify', params);
email.send();
cb(null, verifyOptions.to);
}
}
const options = {
type: 'email',
to: instance.email,
from: {},
redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`,
template: false,
mailer: new Mailer,
host: url[1].split('/')[2],
port: url[2],
protocol: url[0],
user: Self
};
await user.verify(options);
});
Self.remoteMethod('getCurrentUserData', {
description: 'Gets the current user data',
accepts: [
{
arg: 'ctx',
type: 'Object',
type: 'object',
http: {source: 'context'}
}
],
returns: {
type: 'Object',
type: 'object',
root: true
},
http: {
@ -58,7 +107,7 @@ module.exports = Self => {
*
* @param {Integer} userId The user id
* @param {String} name The role name
* @param {Object} options Options
* @param {object} options Options
* @return {Boolean} %true if user has the role, %false otherwise
*/
Self.hasRole = async function(userId, name, options) {
@ -70,8 +119,8 @@ module.exports = Self => {
* Get all user roles.
*
* @param {Integer} userId The user id
* @param {Object} options Options
* @return {Object} User role list
* @param {object} options Options
* @return {object} User role list
*/
Self.getRoles = async(userId, options) => {
let result = await Self.rawSql(

View File

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

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', () => {
it('should return true if the user has the given role', async() => {
let result = await app.models.Account.hasRole(1, 'employee');
let result = await models.Account.hasRole(1, 'employee');
expect(result).toBeTruthy();
});
it('should return false if the user doesnt have the given role', async() => {
let result = await app.models.Account.hasRole(1, 'administrator');
let result = await models.Account.hasRole(1, 'administrator');
expect(result).toBeFalsy();
});

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`
SET `invoiceInFk`=2
WHERE `id`=2;
INSERT INTO `bs`.`salesPerson` (`workerFk`, `year`, `month`, `portfolioWeight`)
VALUES
(18, YEAR(util.VN_CURDATE()), MONTH(util.VN_CURDATE()), 807.23),
(19, YEAR(util.VN_CURDATE()), MONTH(util.VN_CURDATE()), 34.40);
INSERT INTO `bs`.`sale` (`saleFk`, `amount`, `dated`, `typeFk`, `clientFk`)
VALUES

View File

@ -81044,4 +81044,3 @@ USE `vncontrol`;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- 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"]',
firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]'
},
recoverPassword: {
recoverPasswordButton: 'vn-login a[ui-sref="recoverPassword"]',
email: 'vn-recover-password vn-textfield[ng-model="$ctrl.email"]',
sendEmailButton: 'vn-recover-password vn-submit',
},
accountIndex: {
addAccount: 'vn-user-index button vn-icon[icon="add"]',
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',

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 page;
beforeAll(async () => {
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'ticket');
await page.accessToSection('ticket.future');
});
afterAll(async () => {
afterAll(async() => {
await browser.close();
});
const now = new Date();
const tomorrow = new Date(now.getDate() + 1);
const ticket = {
originDated: now,
futureDated: now,
linesMax: '9999',
litersMax: '9999',
warehouseFk: 'Warehouse One'
};
it('should show errors snackbar because of the required data', async () => {
it('should show errors snackbar because of the required data', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.warehouseFk);
await page.waitToClick(selectors.ticketFuture.submit);
let message = await page.waitForSnackbar();
expect(message.text).toContain('warehouseFk is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.litersMax);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
expect(message.text).toContain('litersMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.linesMax);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
expect(message.text).toContain('linesMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.futureDated);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
expect(message.text).toContain('futureDated is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.originDated);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
expect(message.text).toContain('originDated is a required argument');
});
it('should search with the required data', async () => {
it('should search with the required data', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search with the origin shipped today', async () => {
it('should search with the origin shipped today', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, now);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search with the origin shipped tomorrow', async () => {
it('should search with the origin shipped tomorrow', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
it('should search with the destination shipped today', async () => {
it('should search with the destination shipped today', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
await page.pickDate(selectors.ticketFuture.tfShipped, now);
@ -86,14 +84,14 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search with the destination shipped tomorrow', async () => {
it('should search with the destination shipped tomorrow', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
it('should search with the origin IPT', async () => {
it('should search with the origin IPT', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
@ -108,7 +106,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
it('should search with the destination IPT', async () => {
it('should search with the destination IPT', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
@ -123,7 +121,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
it('should search with the origin grouped state', async () => {
it('should search with the origin grouped state', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
@ -138,7 +136,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 3);
});
it('should search with the destination grouped state', async () => {
it('should search with the destination grouped state', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
@ -164,10 +162,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with an ID Origin', async () => {
it('should search in smart-table with an ID Origin', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableId, "13");
await page.keyboard.press("Enter");
await page.write(selectors.ticketFuture.tableId, '13');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 2);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -176,10 +174,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with an ID Destination', async () => {
it('should search in smart-table with an ID Destination', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableTfId, "12");
await page.keyboard.press("Enter");
await page.write(selectors.ticketFuture.tableTfId, '12');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -188,7 +186,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with an IPT Origin', async () => {
it('should search in smart-table with an IPT Origin', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
@ -199,7 +197,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with an IPT Destination', async () => {
it('should search in smart-table with an IPT Destination', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
@ -210,10 +208,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with especified Lines', async () => {
it('should search in smart-table with especified Lines', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLines, "0");
await page.keyboard.press("Enter");
await page.write(selectors.ticketFuture.tableLines, '0');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -222,8 +220,8 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLines, "1");
await page.keyboard.press("Enter");
await page.write(selectors.ticketFuture.tableLines, '1');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -232,10 +230,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with especified Liters', async () => {
it('should search in smart-table with especified Liters', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLiters, "0");
await page.keyboard.press("Enter");
await page.write(selectors.ticketFuture.tableLiters, '0');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -244,8 +242,8 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLiters, "28");
await page.keyboard.press("Enter");
await page.write(selectors.ticketFuture.tableLiters, '28');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@ -254,13 +252,13 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should check the three last tickets and move to the future', async () => {
it('should check the three last tickets and move to the future', async() => {
await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.ticketFuture.acceptButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!');
});
});

View File

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

View File

@ -23,7 +23,10 @@ export default class Auth {
initialize() {
let criteria = {
to: state => state.name != 'login'
to: state => {
const outLayout = ['login', 'recoverPassword', 'resetPassword'];
return !outLayout.some(ol => ol == state.name);
}
};
this.$transitions.onStart(criteria, transition => {
if (this.loggedIn)

View File

@ -1,9 +1,8 @@
<vn-layout
ng-if="$ctrl.showLayout">
</vn-layout>
<ui-view
name="login"
<vn-out-layout
ng-if="!$ctrl.showLayout">
</ui-view>
</vn-out-layout>
<vn-snackbar vn-id="snackbar"></vn-snackbar>
<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
*/
export default class App extends Component {
constructor($element, $, $location, $state) {
super($element, $, $location, $state);
this.$location = $location;
this.$state = $state;
}
$postLink() {
this.vnApp.logger = this;
}
get showLayout() {
let state = this.$state.current.name;
return state && state != 'login';
const state = this.$state.current.name || this.$location.$$path.substring(1).replace('/', '.');
const outLayout = ['login', 'recoverPassword', 'resetPassword'];
return state && !outLayout.some(ol => ol == state);
}
$onDestroy() {

View File

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

View File

@ -37,7 +37,7 @@ export default class Controller extends Section {
const validations = window.validations;
value.forEach(log => {
const locale = validations[log.changedModel].locale ? validations[log.changedModel].locale : {};
const locale = validations[log.changedModel] && validations[log.changedModel].locale ? validations[log.changedModel].locale : {};
log.oldProperties = this.getInstance(log.oldInstance, locale);
log.newProperties = this.getInstance(log.newInstance, locale);

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

@ -2,3 +2,7 @@ User: User
Password: Password
Do not close session: Do not close session
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
Password: Contraseña
Email: Correo electrónico
Do not close session: No cerrar sesión
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">
<img src="./logo.svg"/>
<form name="form" ng-submit="$ctrl.submit()">
<vn-textfield
label="User"
ng-model="$ctrl.user"
vn-id="userField"
vn-focus>
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.password"
type="password">
</vn-textfield>
<vn-check
label="Do not close session"
ng-model="$ctrl.remember"
name="remember">
</vn-check>
<div class="footer">
<vn-submit label="Enter"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>
</form>
<vn-textfield
label="User"
ng-model="$ctrl.user"
vn-id="userField"
vn-focus>
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.password"
type="password">
</vn-textfield>
<vn-check
label="Do not close session"
ng-model="$ctrl.remember"
name="remember">
</vn-check>
<div class="footer">
<vn-submit label="Enter" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
<div class="vn-pt-lg">
<a ui-sref="recoverPassword" translate>
I do not remember my password
</a>
</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";
vn-login {
vn-login,
vn-reset-password,
vn-recover-password{
.footer {
margin-top: 32px;
text-align: center;
position: relative;
& > .vn-submit {
display: block;
& > input {
display: block;
width: 100%;
}
}
& > .spinner-wrapper {
position: absolute;
width: 0;
top: 3px;
right: -8px;
overflow: visible;
}
}
}
vn-out-layout{
position: absolute;
height: 100%;
width: 100%;
@ -39,28 +64,17 @@ vn-login {
white-space: inherit;
}
}
& > .footer {
margin-top: 32px;
text-align: center;
position: relative;
& > vn-submit {
display: block;
& > input {
display: block;
width: 100%;
}
}
& > .spinner-wrapper {
position: absolute;
width: 0;
top: 3px;
right: -8px;
overflow: visible;
}
}
}
h5{
color: $color-primary;
}
.text-secondary{
text-align: center;
padding-bottom: 16px;
}
}
@media screen and (max-width: 600px) {
@ -71,4 +85,8 @@ vn-login {
box-shadow: none;
}
}
a{
color: $color-primary;
}
}

View File

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

View File

@ -9,9 +9,17 @@ function config($stateProvider, $urlRouterProvider) {
.state('login', {
url: '/login?continue',
description: 'Login',
views: {
login: {template: '<vn-login></vn-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', {
url: '/',

View File

@ -131,12 +131,15 @@
"Fichadas impares": "Odd signs",
"Descanso diario 9h.": "Daily rest 9h.",
"Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.",
"Verify email": "Verify email",
"Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Click on the following link to verify this email. If you haven't requested this email, just ignore it",
"Password does not meet requirements": "Password does not meet requirements",
"You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies",
"Not enough privileges to edit a client": "Not enough privileges to edit a client",
"Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*",
"You don't have grant privilege": "You don't have grant privilege",
"You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user",
"Email verify": "Email verify",
"Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})",
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production",
"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",
"There aren't records for this week": "No existen registros para esta semana",
"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",
"This receipt was not compensated": "Este recibo no ha sido compensado",
"Client's email was not found": "No se encontró el email del cliente"

View File

@ -30,5 +30,13 @@
"type": "number",
"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';
export default class Controller extends Section {
$onInit() {
if (this.$params.emailConfirmed)
this.vnApp.showSuccess(this.$t('Email verified successfully!'));
}
onSubmit() {
this.$.watcher.submit()
.then(() => this.card.reload());

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",
"component": "vn-user-basic-data",
"description": "Basic data",

View File

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

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/updateAddress')(Self);
require('../methods/client/updateFiscalData')(Self);
require('../methods/client/updatePortfolio')(Self);
require('../methods/client/updateUser')(Self);
require('../methods/client/uploadFile')(Self);
require('../methods/client/campaignMetricsPdf')(Self);

View File

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

View File

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

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,
d.isTeleworking
FROM tmp.timeBusinessCalculate tb
JOIN user u ON u.id = tb.userFk
JOIN account.user u ON u.id = tb.userFk
JOIN department d ON d.id = tb.departmentFk
JOIN business b ON b.id = tb.businessFk
LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk AND tc.dated = tb.dated
@ -332,18 +332,9 @@ module.exports = Self => {
}, myOptions);
const timestamp = started.getTime() / 1000;
await models.Mail.create({
receiver: previousReceiver,
subject: $t('Record of hours week', {
week: args.week,
year: args.year
}),
body: `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}`
}, myOptions);
const url = `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}`;
query = `INSERT IGNORE INTO workerTimeControlMail (workerFk, year, week)
VALUES (?, ?, ?);`;
await Self.rawSql(query, [previousWorkerFk, args.year, args.week], myOptions);
await models.WorkerTimeControl.weeklyHourRecordEmail(ctx, previousReceiver, args.week, args.year, url);
previousWorkerFk = day.workerFk;
previousReceiver = day.receiver;

View File

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

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/sendMail')(Self);
require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self);
require('../methods/worker-time-control/weeklyHourRecordEmail')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')

111
package-lock.json generated
View File

@ -24,7 +24,7 @@
"jsdom": "^16.7.0",
"jszip": "^3.10.0",
"ldapjs": "^2.2.0",
"loopback": "^3.26.0",
"loopback": "^3.28.0",
"loopback-boot": "3.3.1",
"loopback-component-explorer": "^6.5.0",
"loopback-component-storage": "3.6.1",
@ -4878,13 +4878,20 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001299",
"version": "1.0.30001434",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
"integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==",
"dev": true,
"license": "CC-BY-4.0",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
}
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
}
]
},
"node_modules/canonical-json": {
"version": "0.0.4",
@ -12881,6 +12888,66 @@
"xmlcreate": "^1.0.1"
}
},
"node_modules/jsbarcode": {
"version": "3.11.5",
"resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz",
"integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==",
"bin": {
"auto.js": "bin/barcodes/CODE128/auto.js",
"Barcode.js": "bin/barcodes/Barcode.js",
"barcodes": "bin/barcodes",
"canvas.js": "bin/renderers/canvas.js",
"checksums.js": "bin/barcodes/MSI/checksums.js",
"codabar": "bin/barcodes/codabar",
"CODE128": "bin/barcodes/CODE128",
"CODE128_AUTO.js": "bin/barcodes/CODE128/CODE128_AUTO.js",
"CODE128.js": "bin/barcodes/CODE128/CODE128.js",
"CODE128A.js": "bin/barcodes/CODE128/CODE128A.js",
"CODE128B.js": "bin/barcodes/CODE128/CODE128B.js",
"CODE128C.js": "bin/barcodes/CODE128/CODE128C.js",
"CODE39": "bin/barcodes/CODE39",
"constants.js": "bin/barcodes/ITF/constants.js",
"defaults.js": "bin/options/defaults.js",
"EAN_UPC": "bin/barcodes/EAN_UPC",
"EAN.js": "bin/barcodes/EAN_UPC/EAN.js",
"EAN13.js": "bin/barcodes/EAN_UPC/EAN13.js",
"EAN2.js": "bin/barcodes/EAN_UPC/EAN2.js",
"EAN5.js": "bin/barcodes/EAN_UPC/EAN5.js",
"EAN8.js": "bin/barcodes/EAN_UPC/EAN8.js",
"encoder.js": "bin/barcodes/EAN_UPC/encoder.js",
"ErrorHandler.js": "bin/exceptions/ErrorHandler.js",
"exceptions": "bin/exceptions",
"exceptions.js": "bin/exceptions/exceptions.js",
"fixOptions.js": "bin/help/fixOptions.js",
"GenericBarcode": "bin/barcodes/GenericBarcode",
"getOptionsFromElement.js": "bin/help/getOptionsFromElement.js",
"getRenderProperties.js": "bin/help/getRenderProperties.js",
"help": "bin/help",
"index.js": "bin/renderers/index.js",
"index.tmp.js": "bin/barcodes/index.tmp.js",
"ITF": "bin/barcodes/ITF",
"ITF.js": "bin/barcodes/ITF/ITF.js",
"ITF14.js": "bin/barcodes/ITF/ITF14.js",
"JsBarcode.js": "bin/JsBarcode.js",
"linearizeEncodings.js": "bin/help/linearizeEncodings.js",
"merge.js": "bin/help/merge.js",
"MSI": "bin/barcodes/MSI",
"MSI.js": "bin/barcodes/MSI/MSI.js",
"MSI10.js": "bin/barcodes/MSI/MSI10.js",
"MSI1010.js": "bin/barcodes/MSI/MSI1010.js",
"MSI11.js": "bin/barcodes/MSI/MSI11.js",
"MSI1110.js": "bin/barcodes/MSI/MSI1110.js",
"object.js": "bin/renderers/object.js",
"options": "bin/options",
"optionsFromStrings.js": "bin/help/optionsFromStrings.js",
"pharmacode": "bin/barcodes/pharmacode",
"renderers": "bin/renderers",
"shared.js": "bin/renderers/shared.js",
"svg.js": "bin/renderers/svg.js",
"UPC.js": "bin/barcodes/EAN_UPC/UPC.js",
"UPCE.js": "bin/barcodes/EAN_UPC/UPCE.js"
}
},
"node_modules/jsbn": {
"version": "0.1.1",
"license": "MIT"
@ -23759,6 +23826,14 @@
"version": "1.0.2",
"license": "Apache-2.0"
},
"node_modules/xmldom": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
"integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"license": "MIT",
@ -23938,6 +24013,7 @@
"fs-extra": "^7.0.1",
"intl": "^1.2.5",
"js-yaml": "^3.13.1",
"jsbarcode": "^3.11.5",
"jsonexport": "^3.2.0",
"juice": "^5.2.0",
"log4js": "^6.7.0",
@ -23948,7 +24024,8 @@
"strftime": "^0.10.0",
"vue": "^2.6.10",
"vue-i18n": "^8.15.0",
"vue-server-renderer": "^2.6.10"
"vue-server-renderer": "^2.6.10",
"xmldom": "^0.6.0"
}
},
"print/node_modules/fs-extra": {
@ -28107,7 +28184,9 @@
"version": "1.0.0"
},
"caniuse-lite": {
"version": "1.0.30001299",
"version": "1.0.30001434",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
"integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==",
"dev": true
},
"canonical-json": {
@ -39828,6 +39907,11 @@
"xmlcreate": "^1.0.1"
}
},
"jsbarcode": {
"version": "3.11.5",
"resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz",
"integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA=="
},
"jsbn": {
"version": "0.1.1"
},
@ -57805,6 +57889,7 @@
"fs-extra": "^7.0.1",
"intl": "^1.2.5",
"js-yaml": "^3.13.1",
"jsbarcode": "^3.11.5",
"jsonexport": "^3.2.0",
"juice": "^5.2.0",
"log4js": "^6.7.0",
@ -57815,7 +57900,8 @@
"strftime": "^0.10.0",
"vue": "^2.6.10",
"vue-i18n": "^8.15.0",
"vue-server-renderer": "^2.6.10"
"vue-server-renderer": "^2.6.10",
"xmldom": "^0.6.0"
},
"dependencies": {
"fs-extra": {
@ -59678,6 +59764,11 @@
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz",
"integrity": "sha512-Mbe56Dvj00onbnSo9J0qj/XlY5bfN9KidsOnpd5tRCsR3ekB3hyyNU9fGrTdqNT5ZNvv4BsA2TcQlignsZyVcw=="
},
"xmldom": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
"integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg=="
},
"xtend": {
<<<<<<< HEAD
"version": "1.0.3",

View File

@ -50,3 +50,9 @@
.page-break-after {
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);
},
send(options) {
async send(options) {
options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
if (process.env.NODE_ENV !== 'production') {
const notProductionError = {message: 'This not production, this email not sended'};
await this.mailLog(options, notProductionError);
if (!config.smtp.auth.user)
return Promise.resolve(true);
@ -24,29 +26,35 @@ module.exports = {
throw err;
}).finally(async() => {
const attachments = [];
if (options.attachments) {
for (let attachment of options.attachments) {
const fileName = attachment.filename;
const filePath = attachment.path;
if (fileName.includes('.png')) continue;
if (fileName || filePath)
attachments.push(filePath ? filePath : fileName);
}
}
const fileNames = attachments.join(',\n');
await db.rawSql(`
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
VALUES (?, ?, 1, ?, ?, ?, ?)`, [
options.to,
options.replyTo,
options.subject,
options.text || options.html,
fileNames,
error && error.message || 'Sent'
]);
await this.mailLog(options, error);
});
},
async mailLog(options, error) {
const attachments = [];
if (options.attachments) {
for (let attachment of options.attachments) {
const fileName = attachment.filename;
const filePath = attachment.path;
if (fileName.includes('.png')) continue;
if (fileName || filePath)
attachments.push(filePath ? filePath : fileName);
}
}
const fileNames = attachments.join(',\n');
await db.rawSql(`
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
VALUES (?, ?, 1, ?, ?, ?, ?)`, [
options.to,
options.replyTo,
options.subject,
options.text || options.html,
fileNames,
error && error.message || 'Sent'
]);
}
};

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

View File

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

View File

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

View File

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

View File

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