Merge branch 'master' into 5999-defaultCountry
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
4632ed298d
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2328.01] - 2023-07-13
|
||||
|
||||
### Added
|
||||
- (Clientes -> Morosos) Añadida columna "es trabajador"
|
||||
- (Trabajadores -> Departamentos) Nueva sección
|
||||
- (Trabajadores -> Departamentos) Añadido listado de Trabajadores por departamento
|
||||
- (Trabajadores -> Departamentos) Añadido características de departamento e información
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
- (Trabajadores -> Departamentos) Arreglado búscador
|
||||
|
||||
|
||||
## [2326.01] - 2023-06-29
|
||||
|
||||
### Added
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('signIn', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'user',
|
||||
type: 'String',
|
||||
description: 'The user name or email',
|
||||
required: true
|
||||
}, {
|
||||
arg: 'password',
|
||||
type: 'String',
|
||||
description: 'The password'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/sign-in`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.signIn = async function(ctx, user, password, options) {
|
||||
const myOptions = {};
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const where = Self.userUses(user);
|
||||
const vnUser = await Self.findOne({
|
||||
fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'],
|
||||
where
|
||||
}, myOptions);
|
||||
|
||||
const validCredentials = vnUser
|
||||
&& await vnUser.hasPassword(password);
|
||||
|
||||
if (validCredentials) {
|
||||
if (!vnUser.active)
|
||||
throw new UserError('User disabled');
|
||||
await Self.sendTwoFactor(ctx, vnUser, myOptions);
|
||||
await Self.passExpired(vnUser, myOptions);
|
||||
|
||||
if (vnUser.twoFactor)
|
||||
throw new ForbiddenError(null, 'REQUIRES_2FA');
|
||||
}
|
||||
|
||||
return Self.validateLogin(user, password);
|
||||
};
|
||||
|
||||
Self.passExpired = async(vnUser, myOptions) => {
|
||||
const today = Date.vnNew();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
|
||||
const $ = Self.app.models;
|
||||
const changePasswordToken = await $.AccessToken.create({
|
||||
scopes: ['changePassword'],
|
||||
userId: vnUser.id
|
||||
}, myOptions);
|
||||
const err = new UserError('Pass expired', 'passExpired');
|
||||
changePasswordToken.twoFactor = vnUser.twoFactor ? true : false;
|
||||
err.details = {token: changePasswordToken};
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
Self.sendTwoFactor = async(ctx, vnUser, myOptions) => {
|
||||
if (vnUser.twoFactor === 'email') {
|
||||
const $ = Self.app.models;
|
||||
|
||||
const code = String(Math.floor(Math.random() * 999999));
|
||||
const maxTTL = ((60 * 1000) * 5); // 5 min
|
||||
await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
|
||||
userFk: vnUser.id,
|
||||
code: code,
|
||||
expires: Date.vnNow() + maxTTL
|
||||
}, myOptions);
|
||||
|
||||
const headers = ctx.req.headers;
|
||||
const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
|
||||
const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, '');
|
||||
const params = {
|
||||
args: {
|
||||
recipientId: vnUser.id,
|
||||
recipient: vnUser.email,
|
||||
code: code,
|
||||
ip: ctx.req?.connection?.remoteAddress,
|
||||
device: platform && browser ? platform + ', ' + browser : headers['user-agent'],
|
||||
},
|
||||
req: {getLocale: ctx.req.getLocale},
|
||||
};
|
||||
|
||||
await Self.sendTemplate(params, 'auth-code', true);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,81 +0,0 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('signIn', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'user',
|
||||
type: 'String',
|
||||
description: 'The user name or email',
|
||||
http: {source: 'form'},
|
||||
required: true
|
||||
}, {
|
||||
arg: 'password',
|
||||
type: 'String',
|
||||
description: 'The password'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/signIn`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.signIn = async function(user, password) {
|
||||
const models = Self.app.models;
|
||||
const usesEmail = user.indexOf('@') !== -1;
|
||||
let token;
|
||||
|
||||
const userInfo = usesEmail
|
||||
? {email: user}
|
||||
: {username: user};
|
||||
const instance = await Self.findOne({
|
||||
fields: ['username', 'password'],
|
||||
where: userInfo
|
||||
});
|
||||
|
||||
const where = usesEmail
|
||||
? {email: user}
|
||||
: {name: user};
|
||||
const vnUser = await Self.findOne({
|
||||
fields: ['id', 'active', 'passExpired'],
|
||||
where
|
||||
});
|
||||
|
||||
const today = Date.vnNew();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const validCredentials = instance
|
||||
&& await instance.hasPassword(password);
|
||||
|
||||
if (validCredentials) {
|
||||
if (!vnUser.active)
|
||||
throw new UserError('User disabled');
|
||||
|
||||
if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
|
||||
const changePasswordToken = await models.AccessToken.create({
|
||||
scopes: ['change-password'],
|
||||
userId: vnUser.id
|
||||
});
|
||||
const err = new UserError('Pass expired', 'passExpired');
|
||||
err.details = {token: changePasswordToken};
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await models.Account.sync(instance.username, password);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
let loginInfo = Object.assign({password}, userInfo);
|
||||
token = await Self.login(loginInfo, 'user');
|
||||
return {token: token.id, ttl: token.ttl};
|
||||
};
|
||||
};
|
|
@ -0,0 +1,101 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('VnUser Sign-in()', () => {
|
||||
const employeeId = 1;
|
||||
const unauthCtx = {
|
||||
req: {
|
||||
headers: {},
|
||||
connection: {
|
||||
remoteAddress: '127.0.0.1'
|
||||
},
|
||||
getLocale: () => 'en'
|
||||
},
|
||||
args: {}
|
||||
};
|
||||
const {VnUser, AccessToken} = models;
|
||||
describe('when credentials are correct', () => {
|
||||
it('should return the token', async() => {
|
||||
let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
|
||||
let accessToken = await AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await VnUser.logout(ctx.req.accessToken.id);
|
||||
});
|
||||
|
||||
it('should return the token if the user doesnt exist but the client does', async() => {
|
||||
let login = await VnUser.signIn(unauthCtx, 'PetterParker', 'nightmare');
|
||||
let accessToken = await AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await VnUser.logout(ctx.req.accessToken.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when credentials are incorrect', () => {
|
||||
it('should throw a 401 error', async() => {
|
||||
let error;
|
||||
|
||||
try {
|
||||
await VnUser.signIn(unauthCtx, 'IDontExist', 'TotallyWrongPassword');
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(401);
|
||||
expect(error.code).toBe('LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when two-factor auth is required', () => {
|
||||
it('should throw a 403 error', async() => {
|
||||
const employee = await VnUser.findById(employeeId);
|
||||
const tx = await VnUser.beginTransaction({});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
await employee.updateAttribute('twoFactor', 'email', options);
|
||||
|
||||
await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(403);
|
||||
expect(error.code).toBe('REQUIRES_2FA');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passExpired', () => {
|
||||
it('should throw a passExpired error', async() => {
|
||||
const tx = await VnUser.beginTransaction({});
|
||||
const employee = await VnUser.findById(employeeId);
|
||||
const yesterday = Date.vnNew();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
await employee.updateAttribute('passExpired', yesterday, options);
|
||||
|
||||
await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.message).toBe('Pass expired');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('VnUser signIn()', () => {
|
||||
describe('when credentials are correct', () => {
|
||||
it('should return the token', async() => {
|
||||
let login = await models.VnUser.signIn('salesAssistant', 'nightmare');
|
||||
let accessToken = await models.AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await models.VnUser.logout(ctx.req.accessToken.id);
|
||||
});
|
||||
|
||||
it('should return the token if the user doesnt exist but the client does', async() => {
|
||||
let login = await models.VnUser.signIn('PetterParker', 'nightmare');
|
||||
let accessToken = await models.AccessToken.findById(login.token);
|
||||
let ctx = {req: {accessToken: accessToken}};
|
||||
|
||||
expect(login.token).toBeDefined();
|
||||
|
||||
await models.VnUser.logout(ctx.req.accessToken.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when credentials are incorrect', () => {
|
||||
it('should throw a 401 error', async() => {
|
||||
let error;
|
||||
|
||||
try {
|
||||
await models.VnUser.signIn('IDontExist', 'TotallyWrongPassword');
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(401);
|
||||
expect(error.code).toBe('LOGIN_FAILED');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('VnUser validate-auth()', () => {
|
||||
describe('validateAuth', () => {
|
||||
it('should signin if data is correct', async() => {
|
||||
await models.AuthCode.create({
|
||||
userFk: 9,
|
||||
code: '555555',
|
||||
expires: Date.vnNow() + (60 * 1000)
|
||||
});
|
||||
const token = await models.VnUser.validateAuth('developer', 'nightmare', '555555');
|
||||
|
||||
expect(token.token).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCode', () => {
|
||||
it('should throw an error for a non existent code', async() => {
|
||||
let error;
|
||||
try {
|
||||
await models.VnUser.validateCode('developer', '123456');
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.message).toEqual('Invalid or expired verification code');
|
||||
});
|
||||
|
||||
it('should throw an error when a code doesn`t match the login username', async() => {
|
||||
let error;
|
||||
let authCode;
|
||||
try {
|
||||
authCode = await models.AuthCode.create({
|
||||
userFk: 1,
|
||||
code: '555555',
|
||||
expires: Date.vnNow() + (60 * 1000)
|
||||
});
|
||||
|
||||
await models.VnUser.validateCode('developer', '555555');
|
||||
} catch (e) {
|
||||
authCode && await authCode.destroy();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.message).toEqual('Authentication failed');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('validateAuth', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'user',
|
||||
type: 'String',
|
||||
description: 'The user name or email',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
arg: 'password',
|
||||
type: 'String',
|
||||
description: 'The password'
|
||||
},
|
||||
{
|
||||
arg: 'code',
|
||||
type: 'String',
|
||||
description: 'The auth code'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/validate-auth`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.validateAuth = async(username, password, code, options) => {
|
||||
const myOptions = {};
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const token = Self.validateLogin(username, password);
|
||||
await Self.validateCode(username, code, myOptions);
|
||||
return token;
|
||||
};
|
||||
|
||||
Self.validateCode = async(username, code, myOptions) => {
|
||||
const {AuthCode} = Self.app.models;
|
||||
|
||||
const authCode = await AuthCode.findOne({
|
||||
where: {
|
||||
code: code
|
||||
}
|
||||
}, myOptions);
|
||||
|
||||
const expired = authCode && Date.vnNow() > authCode.expires;
|
||||
if (!authCode || expired)
|
||||
throw new UserError('Invalid or expired verification code');
|
||||
|
||||
const user = await Self.findById(authCode.userFk, {
|
||||
fields: ['name', 'twoFactor']
|
||||
}, myOptions);
|
||||
|
||||
if (user.name !== username)
|
||||
throw new UserError('Authentication failed');
|
||||
|
||||
await authCode.destroy(myOptions);
|
||||
};
|
||||
};
|
|
@ -1,7 +1,4 @@
|
|||
{
|
||||
"AccountingType": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"AccessTokenConfig": {
|
||||
"dataSource": "vn",
|
||||
"options": {
|
||||
|
@ -10,6 +7,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"AccountingType": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"AuthCode": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"Bank": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "AuthCode",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "salix.authCode"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"userFk": {
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"id": true
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"expires": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "Account",
|
||||
"foreignKey": "userFk"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,11 +5,12 @@ const {Email} = require('vn-print');
|
|||
module.exports = function(Self) {
|
||||
vnModel(Self);
|
||||
|
||||
require('../methods/vn-user/signIn')(Self);
|
||||
require('../methods/vn-user/sign-in')(Self);
|
||||
require('../methods/vn-user/acl')(Self);
|
||||
require('../methods/vn-user/recover-password')(Self);
|
||||
require('../methods/vn-user/validate-token')(Self);
|
||||
require('../methods/vn-user/privileges')(Self);
|
||||
require('../methods/vn-user/validate-auth')(Self);
|
||||
require('../methods/vn-user/renew-token')(Self);
|
||||
|
||||
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
|
||||
|
@ -111,6 +112,18 @@ module.exports = function(Self) {
|
|||
return email.send();
|
||||
});
|
||||
|
||||
Self.validateLogin = async function(user, password) {
|
||||
let loginInfo = Object.assign({password}, Self.userUses(user));
|
||||
token = await Self.login(loginInfo, 'user');
|
||||
return {token: token.id, ttl: token.ttl};
|
||||
};
|
||||
|
||||
Self.userUses = function(user) {
|
||||
return user.indexOf('@') !== -1
|
||||
? {email: user}
|
||||
: {username: user};
|
||||
};
|
||||
|
||||
const _setPassword = Self.prototype.setPassword;
|
||||
Self.prototype.setPassword = async function(newPassword, options, cb) {
|
||||
if (cb === undefined && typeof options === 'function') {
|
||||
|
@ -143,8 +156,9 @@ module.exports = function(Self) {
|
|||
}
|
||||
};
|
||||
|
||||
Self.sharedClass._methods.find(method => method.name == 'changePassword')
|
||||
.accessScopes = ['change-password'];
|
||||
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls =
|
||||
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
|
||||
.filter(acl => acl.property != 'changePassword');
|
||||
|
||||
// FIXME: https://redmine.verdnatura.es/issues/5761
|
||||
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
|
||||
|
|
|
@ -59,7 +59,10 @@
|
|||
},
|
||||
"passExpired": {
|
||||
"type": "date"
|
||||
}
|
||||
},
|
||||
"twoFactor": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"role": {
|
||||
|
@ -111,6 +114,13 @@
|
|||
"principalId": "$authenticated",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "validateAuth",
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "privileges",
|
||||
"accessType": "*",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
DELETE FROM `salix`.`ACL` WHERE model = 'MailAliasAccount';
|
||||
|
||||
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
|
||||
VALUES
|
||||
('MailAliasAccount', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
|
||||
('MailAliasAccount', 'create', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
|
||||
('MailAliasAccount', 'deleteById', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
|
||||
('MailAliasAccount', 'canEditAlias', 'WRITE', 'ALLOW', 'ROLE', 'itManagement');
|
|
@ -0,0 +1,13 @@
|
|||
create table `salix`.`authCode`
|
||||
(
|
||||
userFk int UNSIGNED not null,
|
||||
code int not null,
|
||||
expires bigint not null,
|
||||
constraint authCode_pk
|
||||
primary key (userFk),
|
||||
constraint authCode_unique
|
||||
unique (code),
|
||||
constraint authCode_user_id_fk
|
||||
foreign key (userFk) references `account`.`user` (id)
|
||||
on update cascade on delete cascade
|
||||
);
|
|
@ -0,0 +1,89 @@
|
|||
DROP PROCEDURE IF EXISTS `vn`.`clientCreate`;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`client_create`(
|
||||
vFirstname VARCHAR(50),
|
||||
vSurnames VARCHAR(50),
|
||||
vFi VARCHAR(9),
|
||||
vAddress TEXT,
|
||||
vPostcode CHAR(5),
|
||||
vCity VARCHAR(25),
|
||||
vProvinceFk SMALLINT(5),
|
||||
vCompanyFk SMALLINT(5),
|
||||
vPhone VARCHAR(11),
|
||||
vEmail VARCHAR(255),
|
||||
vUserFk INT
|
||||
)
|
||||
BEGIN
|
||||
/**
|
||||
* Create new client
|
||||
*
|
||||
* @params vFirstname firstName
|
||||
* @params vSurnames surnames
|
||||
* @params vFi company code from accounting transactions
|
||||
* @params vAddress address
|
||||
* @params vPostcode postCode
|
||||
* @params vCity city
|
||||
* @params vProvinceFk province
|
||||
* @params vCompanyFk company in which he has become a client
|
||||
* @params vPhone telephone number
|
||||
* @params vEmail email address
|
||||
* @params vUserFk user id
|
||||
*/
|
||||
DECLARE vPayMethodFk INT;
|
||||
DECLARE vDueDay INT;
|
||||
DECLARE vDefaultCredit DECIMAL(10, 2);
|
||||
DECLARE vIsTaxDataChecked TINYINT(1);
|
||||
DECLARE vHasCoreVnl BOOLEAN;
|
||||
DECLARE vMandateTypeFk INT;
|
||||
|
||||
SELECT defaultPayMethodFk,
|
||||
defaultDueDay,
|
||||
defaultCredit,
|
||||
defaultIsTaxDataChecked,
|
||||
defaultHasCoreVnl,
|
||||
defaultMandateTypeFk
|
||||
INTO vPayMethodFk,
|
||||
vDueDay,
|
||||
vDefaultCredit,
|
||||
vIsTaxDataChecked,
|
||||
vHasCoreVnl,
|
||||
vMandateTypeFk
|
||||
FROM clientConfig;
|
||||
|
||||
INSERT INTO `client`
|
||||
SET id = vUserFk,
|
||||
name = CONCAT(vFirstname, ' ', vSurnames),
|
||||
street = vAddress,
|
||||
fi = TRIM(vFi),
|
||||
phone = vPhone,
|
||||
email = vEmail,
|
||||
provinceFk = vProvinceFk,
|
||||
city = vCity,
|
||||
postcode = vPostcode,
|
||||
socialName = CONCAT(vSurnames, ' ', vFirstname),
|
||||
payMethodFk = vPayMethodFk,
|
||||
dueDay = vDueDay,
|
||||
credit = vDefaultCredit,
|
||||
isTaxDataChecked = vIsTaxDataChecked,
|
||||
hasCoreVnl = vHasCoreVnl,
|
||||
isEqualizated = FALSE
|
||||
ON duplicate KEY UPDATE
|
||||
payMethodFk = vPayMethodFk,
|
||||
dueDay = vDueDay,
|
||||
credit = vDefaultCredit,
|
||||
isTaxDataChecked = vIsTaxDataChecked,
|
||||
hasCoreVnl = vHasCoreVnl,
|
||||
isActive = TRUE;
|
||||
|
||||
INSERT INTO mandate (clientFk, companyFk, mandateTypeFk)
|
||||
SELECT vUserFk, vCompanyFk, vMandateTypeFk
|
||||
WHERE NOT EXISTS (
|
||||
SELECT id
|
||||
FROM mandate
|
||||
WHERE clientFk = vUserFk
|
||||
AND companyFk = vCompanyFk
|
||||
AND mandateTypeFk = vMandateTypeFk
|
||||
);
|
||||
END$$
|
||||
DELIMITER ;
|
|
@ -0,0 +1,17 @@
|
|||
ALTER TABLE `vn`.`clientConfig` ADD defaultPayMethodFk tinyint(3) unsigned NULL;
|
||||
ALTER TABLE `vn`.`clientConfig` ADD defaultDueDay int unsigned NULL;
|
||||
ALTER TABLE `vn`.`clientConfig` ADD defaultCredit decimal(10, 2) NULL;
|
||||
ALTER TABLE `vn`.`clientConfig` ADD defaultIsTaxDataChecked tinyint(1) NULL;
|
||||
ALTER TABLE `vn`.`clientConfig` ADD defaultHasCoreVnl boolean NULL;
|
||||
ALTER TABLE `vn`.`clientConfig` ADD defaultMandateTypeFk smallint(5) NULL;
|
||||
ALTER TABLE `vn`.`clientConfig` ADD CONSTRAINT clientNewConfigPayMethod_FK FOREIGN KEY (defaultPayMethodFk) REFERENCES vn.payMethod(id);
|
||||
ALTER TABLE `vn`.`clientConfig` ADD CONSTRAINT clientNewConfigMandateType_FK FOREIGN KEY (defaultMandateTypeFk) REFERENCES vn.mandateType(id);
|
||||
|
||||
UPDATE `vn`.`clientConfig`
|
||||
SET defaultPayMethodFk = 4,
|
||||
defaultDueDay = 5,
|
||||
defaultCredit = 300.0,
|
||||
defaultIsTaxDataChecked = 1,
|
||||
defaultHasCoreVnl = 1,
|
||||
defaultMandateTypeFk = 2
|
||||
WHERE id = 1;
|
|
@ -0,0 +1,24 @@
|
|||
alter table `vn`.`department`
|
||||
add `twoFactor` ENUM ('email') null comment 'Default user two-factor auth type';
|
||||
|
||||
drop trigger `vn`.`department_afterUpdate`;
|
||||
|
||||
DELIMITER $$
|
||||
$$
|
||||
create definer = root@localhost trigger `vn`.`department_afterUpdate`
|
||||
after update
|
||||
on department
|
||||
for each row
|
||||
BEGIN
|
||||
IF !(OLD.parentFk <=> NEW.parentFk) THEN
|
||||
UPDATE vn.department_recalc SET isChanged = TRUE;
|
||||
END IF;
|
||||
|
||||
IF !(OLD.twoFactor <=> NEW.twoFactor) THEN
|
||||
UPDATE account.user u
|
||||
JOIN vn.workerDepartment wd ON wd.workerFk = u.id
|
||||
SET u.twoFactor = NEW.twoFactor
|
||||
WHERE wd.departmentFk = NEW.id;
|
||||
END IF;
|
||||
END;$$
|
||||
DELIMITER ;
|
|
@ -0,0 +1,13 @@
|
|||
UPDATE `salix`.`ACL`
|
||||
SET principalId='financialBoss'
|
||||
WHERE
|
||||
model = 'Client'
|
||||
AND property = 'editCredit';
|
||||
|
||||
UPDATE `salix`.`ACL`
|
||||
SET property='zeroCreditEditor'
|
||||
WHERE
|
||||
model = 'Client'
|
||||
AND property = 'isNotEditableCredit';
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
alter table `account`.`user`
|
||||
add `twoFactor` ENUM ('email') null comment 'Two-factor auth type';
|
||||
|
||||
DELETE FROM `salix`.`ACL`
|
||||
WHERE model = 'VnUser' AND property = 'changePassword';
|
File diff suppressed because one or more lines are too long
|
@ -77,7 +77,10 @@ INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `
|
|||
ORDER BY id;
|
||||
|
||||
INSERT INTO `account`.`account`(`id`)
|
||||
SELECT id FROM `account`.`user`;
|
||||
SELECT `u`.`id`
|
||||
FROM `account`.`user` `u`
|
||||
JOIN `account`.`role` `r` ON `u`.`role` = `r`.`id`
|
||||
WHERE `r`.`name` <> 'customer';
|
||||
|
||||
INSERT INTO `vn`.`educationLevel` (`id`, `name`)
|
||||
VALUES
|
||||
|
@ -144,17 +147,17 @@ INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`)
|
|||
(3, 'GBP', 'Libra', 1),
|
||||
(4, 'JPY', 'Yen Japones', 1);
|
||||
|
||||
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`, `politicalCountryFk`)
|
||||
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`)
|
||||
VALUES
|
||||
(1, 'España', 1, 'ES', 1, 24, 4, 0, 1, 1),
|
||||
(2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1, 2),
|
||||
(3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1, 3),
|
||||
(4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1, 4),
|
||||
(5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1, 5),
|
||||
(8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1, 8),
|
||||
(13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2, 13),
|
||||
(19,'Francia', 1, 'FR', 1, 27, 4, 0, 1, 19),
|
||||
(30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2, 30);
|
||||
(1, 'España', 1, 'ES', 1, 24, 4, 0, 1),
|
||||
(2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1),
|
||||
(3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1),
|
||||
(4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1),
|
||||
(5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1),
|
||||
(8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1),
|
||||
(13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2),
|
||||
(19,'Francia', 1, 'FR', 1, 27, 4, 0, 1),
|
||||
(30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2);
|
||||
|
||||
INSERT INTO `vn`.`warehouseAlias`(`id`, `name`)
|
||||
VALUES
|
||||
|
@ -185,13 +188,13 @@ INSERT INTO `vn`.`printer` (`id`, `name`, `path`, `isLabeler`, `sectorFk`, `ipAd
|
|||
|
||||
UPDATE `vn`.`sector` SET mainPrinterFk = 1 WHERE id = 1;
|
||||
|
||||
INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`, `sectorFk`, `labelerFk`)
|
||||
INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`)
|
||||
VALUES
|
||||
(1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106, NULL, NULL),
|
||||
(1107, 'ANT', 'Hank' , 'Pym' , 1107, 19, 432978107, NULL, NULL),
|
||||
(1108, 'DCX', 'Charles' , 'Xavier', 1108, 19, 432978108, 1, NULL),
|
||||
(1109, 'HLK', 'Bruce' , 'Banner', 1109, 19, 432978109, 1, NULL),
|
||||
(1110, 'JJJ', 'Jessica' , 'Jones' , 1110, 19, 432978110, 2, NULL);
|
||||
(1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106),
|
||||
(1107, 'ANT', 'Hank' , 'Pym' , 1107, 19, 432978107),
|
||||
(1108, 'DCX', 'Charles' , 'Xavier', 1108, 19, 432978108),
|
||||
(1109, 'HLK', 'Bruce' , 'Banner', 1109, 19, 432978109),
|
||||
(1110, 'JJJ', 'Jessica' , 'Jones' , 1110, 19, 432978110);
|
||||
|
||||
INSERT INTO `vn`.`parking` (`id`, `column`, `row`, `sectorFk`, `code`, `pickingOrder`)
|
||||
VALUES
|
||||
|
@ -382,9 +385,16 @@ INSERT INTO `vn`.`clientManaCache`(`clientFk`, `mana`, `dated`)
|
|||
(1103, 0, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
|
||||
(1104, -30, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH));
|
||||
|
||||
INSERT INTO `vn`.`clientConfig`(`riskTolerance`, `maxCreditRows`)
|
||||
INSERT INTO `vn`.`mandateType`(`id`, `name`)
|
||||
VALUES
|
||||
(200, 10);
|
||||
(1, 'B2B'),
|
||||
(2, 'CORE'),
|
||||
(3, 'LCR');
|
||||
|
||||
INSERT INTO `vn`.`clientConfig`(`id`, `riskTolerance`, `maxCreditRows`, `maxPriceIncreasingRatio`, `riskScope`, `defaultPayMethodFk`, `defaultDueDay`, `defaultCredit`, `defaultIsTaxDataChecked`, `defaultHasCoreVnl`, `defaultMandateTypeFk`)
|
||||
VALUES
|
||||
(1, 200, 10, 0.25, 2, 4, 5, 300.00, 1, 1, 2);
|
||||
|
||||
|
||||
INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `phone`, `mobile`, `isActive`, `clientFk`, `agencyModeFk`, `longitude`, `latitude`, `isEqualizated`, `isDefaultAddress`)
|
||||
VALUES
|
||||
|
@ -550,10 +560,13 @@ INSERT INTO `vn`.`supplierAddress`(`id`, `supplierFk`, `nickname`, `street`, `pr
|
|||
|
||||
INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`, `commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`, `workerFk`, `supplierActivityFk`, `isPayMethodChecked`, `healthRegister`)
|
||||
VALUES
|
||||
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
|
||||
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 0, util.VN_CURDATE(), 1, 'supplier address 2', 'GOTHAM', 2, 43022, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
|
||||
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
|
||||
(1381, 'Ornamentales', 'Ornamentales', 7185000440, 1, '03815934E', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
|
||||
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
|
||||
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 0, util.VN_CURDATE(), 1, 'supplier address 2', 'GOTHAM', 2, 43022, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
|
||||
(69, 'Packaging', 'Packaging nick', 4100000069, 1, '94935005K', 0, util.VN_CURDATE(), 1, 'supplier address 5', 'ASGARD', 3, 46600, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
|
||||
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
|
||||
(567, 'Holland', 'Holland nick', 4000020567, 1, '14364089Z', 0, util.VN_CURDATE(), 1, 'supplier address 6', 'ASGARD', 3, 46600, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
|
||||
(791, 'Bros SL', 'Bros nick', 5115000791, 1, '37718083S', 0, util.VN_CURDATE(), 1, 'supplier address 7', 'ASGARD', 3, 46600, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
|
||||
(1381, 'Ornamentales', 'Ornamentales', 7185001381, 1, '07972486L', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
|
||||
|
||||
INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
|
||||
VALUES
|
||||
|
@ -821,12 +834,6 @@ INSERT INTO `vn`.`greuge`(`id`, `clientFk`, `description`, `amount`, `shipped`,
|
|||
(11, 1101, 'some heritage charges', -15.99, DATE_ADD(util.VN_CURDATE(), INTERVAL 1 MONTH), util.VN_CURDATE(), 5, 1),
|
||||
(12, 1101, 'some miscellaneous charges', 58.00, DATE_ADD(util.VN_CURDATE(), INTERVAL 1 MONTH), util.VN_CURDATE(), 6, 1);
|
||||
|
||||
INSERT INTO `vn`.`mandateType`(`id`, `name`)
|
||||
VALUES
|
||||
(1, 'B2B'),
|
||||
(2, 'CORE'),
|
||||
(3, 'LCR');
|
||||
|
||||
INSERT INTO `vn`.`mandate`(`id`, `clientFk`, `companyFk`, `code`, `created`, `mandateTypeFk`)
|
||||
VALUES
|
||||
(1, 1102, 442, '1-1', util.VN_CURDATE(), 2);
|
||||
|
@ -2590,7 +2597,7 @@ UPDATE `vn`.`ticket`
|
|||
|
||||
UPDATE `vn`.`ticket`
|
||||
SET refFk = 'A1111111'
|
||||
WHERE id = 6;
|
||||
WHERE id = 6;
|
||||
|
||||
INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
|
||||
VALUES
|
||||
|
@ -2599,7 +2606,7 @@ INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
|
|||
(3, 6, 5),
|
||||
(4, 7, 1);
|
||||
|
||||
INSERT INTO `vn`.`expeditionTruck` (`id`, `ETD`, `description`)
|
||||
INSERT INTO `vn`.`expeditionTruck` (`id`, `eta`, `description`)
|
||||
VALUES
|
||||
(1, CONCAT(YEAR(DATE_ADD(util.VN_CURDATE(), INTERVAL +3 YEAR))), 'Best truck in fleet');
|
||||
|
||||
|
@ -2845,8 +2852,8 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
|
|||
|
||||
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
|
||||
VALUES
|
||||
('lilium', 'dev', 'http://localhost:9000/#/'),
|
||||
('salix', 'dev', 'http://localhost:5000/#!/');
|
||||
('lilium', 'development', 'http://localhost:9000/#/'),
|
||||
('salix', 'development', 'http://localhost:5000/#!/');
|
||||
|
||||
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
|
||||
VALUES
|
||||
|
@ -2862,7 +2869,8 @@ INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk
|
|||
|
||||
INSERT INTO `vn`.`ticketRefund`(`refundTicketFk`, `originalTicketFk`)
|
||||
VALUES
|
||||
(1, 12);
|
||||
(1, 12),
|
||||
(8, 10);
|
||||
|
||||
INSERT INTO `vn`.`deviceProductionModels` (`code`)
|
||||
VALUES
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -312,8 +312,8 @@ export default {
|
|||
clientDefaulter: {
|
||||
anyClient: 'vn-client-defaulter tbody > tr',
|
||||
firstClientName: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(2) > span',
|
||||
firstSalesPersonName: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(3) > span',
|
||||
firstObservation: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(8) > vn-textarea[ng-model="defaulter.observation"]',
|
||||
firstSalesPersonName: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(4) > span',
|
||||
firstObservation: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(9) > vn-textarea[ng-model="defaulter.observation"]',
|
||||
allDefaulterCheckbox: 'vn-client-defaulter thead vn-multi-check',
|
||||
addObservationButton: 'vn-client-defaulter vn-button[icon="icon-notes"]',
|
||||
observation: '.vn-dialog.shown vn-textarea[ng-model="$ctrl.defaulter.observation"]',
|
||||
|
@ -894,6 +894,18 @@ export default {
|
|||
extension: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(5) > section > span',
|
||||
|
||||
},
|
||||
department: {
|
||||
firstDepartment: 'vn-worker-department-index vn-card > vn-treeview vn-treeview-childs vn-treeview-childs vn-treeview-childs a'
|
||||
},
|
||||
departmentSummary: {
|
||||
header: 'vn-worker-department-summary h5',
|
||||
name: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(1) > section > span',
|
||||
code: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(2) > section > span',
|
||||
chat: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(3) > section > span',
|
||||
bossDepartment: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(4) > section > span',
|
||||
email: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(5) > section > span',
|
||||
clientFk: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(6) > section > span',
|
||||
},
|
||||
workerBasicData: {
|
||||
name: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.firstName"]',
|
||||
surname: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.lastName"]',
|
||||
|
@ -901,6 +913,13 @@ export default {
|
|||
locker: 'vn-worker-basic-data vn-input-number[ng-model="$ctrl.worker.locker"]',
|
||||
saveButton: 'vn-worker-basic-data button[type=submit]'
|
||||
},
|
||||
departmentBasicData: {
|
||||
Name: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.name"]',
|
||||
Code: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.code"]',
|
||||
Chat: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.chat"]',
|
||||
Email: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.notificationEmail"]',
|
||||
saveButton: 'vn-worker-department-basic-data button[type=submit]'
|
||||
},
|
||||
workerNotes: {
|
||||
addNoteFloatButton: 'vn-worker-note vn-icon[icon="add"]',
|
||||
note: 'vn-note-worker-create vn-textarea[ng-model="$ctrl.note.text"]',
|
||||
|
|
|
@ -16,6 +16,7 @@ describe('ChangePassword path', async() => {
|
|||
await browser.close();
|
||||
});
|
||||
|
||||
const badPassword = 'badpass';
|
||||
const oldPassword = 'nightmare';
|
||||
const newPassword = 'newPass.1234';
|
||||
describe('Bad login', async() => {
|
||||
|
@ -37,13 +38,22 @@ describe('ChangePassword path', async() => {
|
|||
expect(message.text).toContain('Invalid current password');
|
||||
|
||||
// Bad attempt: password not meet requirements
|
||||
message = await page.sendForm($.form, {
|
||||
oldPassword: oldPassword,
|
||||
newPassword: badPassword,
|
||||
repeatPassword: badPassword
|
||||
});
|
||||
|
||||
expect(message.text).toContain('Password does not meet requirements');
|
||||
|
||||
// Bad attempt: same password
|
||||
message = await page.sendForm($.form, {
|
||||
oldPassword: oldPassword,
|
||||
newPassword: oldPassword,
|
||||
repeatPassword: oldPassword
|
||||
});
|
||||
|
||||
expect(message.text).toContain('Password does not meet requirements');
|
||||
expect(message.text).toContain('You can not use the same password');
|
||||
|
||||
// Correct attempt: change password
|
||||
message = await page.sendForm($.form, {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import selectors from '../../../helpers/selectors.js';
|
||||
import getBrowser from '../../../helpers/puppeteer';
|
||||
|
||||
describe('department summary path', () => {
|
||||
let browser;
|
||||
let page;
|
||||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('hr', 'worker');
|
||||
await page.accessToSection('worker.department');
|
||||
await page.doSearch('INFORMATICA');
|
||||
await page.click(selectors.department.firstDepartment);
|
||||
});
|
||||
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it('should reach the employee summary section and check all properties', async() => {
|
||||
expect(await page.waitToGetProperty(selectors.departmentSummary.header, 'innerText')).toEqual('INFORMATICA');
|
||||
expect(await page.getProperty(selectors.departmentSummary.name, 'innerText')).toEqual('INFORMATICA');
|
||||
expect(await page.getProperty(selectors.departmentSummary.code, 'innerText')).toEqual('it');
|
||||
expect(await page.getProperty(selectors.departmentSummary.chat, 'innerText')).toEqual('informatica-cau');
|
||||
expect(await page.getProperty(selectors.departmentSummary.bossDepartment, 'innerText')).toEqual('');
|
||||
expect(await page.getProperty(selectors.departmentSummary.email, 'innerText')).toEqual('-');
|
||||
expect(await page.getProperty(selectors.departmentSummary.clientFk, 'innerText')).toEqual('-');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import getBrowser from '../../../helpers/puppeteer';
|
||||
import selectors from '../../../helpers/selectors.js';
|
||||
|
||||
const $ = {
|
||||
form: 'vn-worker-department-basic-data form',
|
||||
};
|
||||
|
||||
describe('department summary path', () => {
|
||||
let browser;
|
||||
let page;
|
||||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('hr', 'worker');
|
||||
await page.accessToSection('worker.department');
|
||||
await page.doSearch('INFORMATICA');
|
||||
await page.click(selectors.department.firstDepartment);
|
||||
});
|
||||
|
||||
beforeEach(async() => {
|
||||
await page.accessToSection('worker.department.card.basicData');
|
||||
});
|
||||
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it(`should edit the department basic data and confirm the department data was edited`, async() => {
|
||||
const values = {
|
||||
Name: 'Informatica',
|
||||
Code: 'IT',
|
||||
Chat: 'informatica-cau',
|
||||
Email: 'it@verdnatura.es',
|
||||
};
|
||||
|
||||
await page.fillForm($.form, values);
|
||||
const formValues = await page.fetchForm($.form, Object.keys(values));
|
||||
const message = await page.sendForm($.form, values);
|
||||
|
||||
expect(message.isSuccess).toBeTrue();
|
||||
expect(formValues).toEqual(values);
|
||||
});
|
||||
});
|
|
@ -19,7 +19,6 @@ describe('Item edit tax path', () => {
|
|||
it(`should add the item tax to all countries`, async() => {
|
||||
await page.autocompleteSearch(selectors.itemTax.firstClass, 'General VAT');
|
||||
await page.autocompleteSearch(selectors.itemTax.secondClass, 'General VAT');
|
||||
await page.autocompleteSearch(selectors.itemTax.thirdClass, 'General VAT');
|
||||
await page.waitToClick(selectors.itemTax.submitTaxButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
|
@ -40,13 +39,6 @@ describe('Item edit tax path', () => {
|
|||
expect(secondVatType).toEqual('General VAT');
|
||||
});
|
||||
|
||||
it(`should confirm the third item tax class was edited`, async() => {
|
||||
const thirdVatType = await page
|
||||
.waitToGetProperty(selectors.itemTax.thirdClass, 'value');
|
||||
|
||||
expect(thirdVatType).toEqual('General VAT');
|
||||
});
|
||||
|
||||
it(`should edit the first class without saving the form`, async() => {
|
||||
await page.autocompleteSearch(selectors.itemTax.firstClass, 'Reduced VAT');
|
||||
const firstVatType = await page.waitToGetProperty(selectors.itemTax.firstClass, 'value');
|
||||
|
|
|
@ -81,6 +81,6 @@ describe('Account Role create and basic data path', () => {
|
|||
await page.accessToSection('account.role.card.inherited');
|
||||
const rolesCount = await page.countElement(selectors.accountRoleInheritance.anyResult);
|
||||
|
||||
expect(rolesCount).toEqual(6);
|
||||
expect(rolesCount).toEqual(7);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ export default class Auth {
|
|||
initialize() {
|
||||
let criteria = {
|
||||
to: state => {
|
||||
const outLayout = ['login', 'recover-password', 'reset-password', 'change-password'];
|
||||
const outLayout = ['login', 'recover-password', 'reset-password', 'change-password', 'validate-email'];
|
||||
return !outLayout.some(ol => ol == state.name);
|
||||
}
|
||||
};
|
||||
|
@ -60,7 +60,25 @@ export default class Auth {
|
|||
};
|
||||
|
||||
const now = new Date();
|
||||
return this.$http.post('VnUsers/signIn', params)
|
||||
return this.$http.post('VnUsers/sign-in', params).then(
|
||||
json => this.onLoginOk(json, now, remember));
|
||||
}
|
||||
|
||||
validateCode(user, password, code, remember) {
|
||||
if (!user) {
|
||||
let err = new UserError('Please enter your username');
|
||||
err.code = 'EmptyLogin';
|
||||
return this.$q.reject(err);
|
||||
}
|
||||
|
||||
let params = {
|
||||
user: user,
|
||||
password: password || undefined,
|
||||
code: code
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
return this.$http.post('VnUsers/validate-auth', params)
|
||||
.then(json => this.onLoginOk(json, now, remember));
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ export default class Token {
|
|||
remember
|
||||
});
|
||||
this.vnInterceptor.setToken(token);
|
||||
|
||||
try {
|
||||
if (remember)
|
||||
this.setStorage(localStorage, token, created, ttl);
|
||||
|
@ -84,7 +83,6 @@ export default class Token {
|
|||
this.renewPeriod = data.renewPeriod;
|
||||
this.stopRenewer();
|
||||
this.inservalId = setInterval(() => this.checkValidity(), data.renewInterval * 1000);
|
||||
this.checkValidity();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,14 @@
|
|||
type="password"
|
||||
autocomplete="false">
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
ng-if="$ctrl.$state.params.twoFactor == 'true'"
|
||||
label="Verification code"
|
||||
ng-model="$ctrl.code"
|
||||
vn-name="code"
|
||||
autocomplete="false"
|
||||
class="vn-mt-md">
|
||||
</vn-textfield>
|
||||
<div class="footer">
|
||||
<vn-submit label="Change password" ng-click="$ctrl.submit()"></vn-submit>
|
||||
<div class="spinner-wrapper">
|
||||
|
|
|
@ -26,11 +26,13 @@ export default class Controller {
|
|||
|
||||
submit() {
|
||||
const userId = this.$state.params.userId;
|
||||
const newPassword = this.newPassword;
|
||||
const oldPassword = this.oldPassword;
|
||||
const newPassword = this.newPassword;
|
||||
const repeatPassword = this.repeatPassword;
|
||||
const code = this.code;
|
||||
|
||||
if (!newPassword)
|
||||
throw new UserError(`You must enter a new password`);
|
||||
if (!oldPassword || !newPassword || !repeatPassword)
|
||||
throw new UserError(`You must fill all the fields`);
|
||||
if (newPassword != this.repeatPassword)
|
||||
throw new UserError(`Passwords don't match`);
|
||||
|
||||
|
@ -38,11 +40,12 @@ export default class Controller {
|
|||
Authorization: this.$state.params.id
|
||||
};
|
||||
|
||||
this.$http.post('VnUsers/change-password',
|
||||
this.$http.patch('Accounts/change-password',
|
||||
{
|
||||
id: userId,
|
||||
oldPassword,
|
||||
newPassword
|
||||
newPassword,
|
||||
code
|
||||
},
|
||||
{headers}
|
||||
).then(() => {
|
||||
|
|
|
@ -2,6 +2,10 @@ Change password: Cambiar contraseña
|
|||
Old password: Antigua contraseña
|
||||
New password: Nueva contraseña
|
||||
Repeat password: Repetir contraseña
|
||||
Passwords don't match: Las contraseñas no coinciden
|
||||
You must fill all the fields: Debes rellenar todos los campos
|
||||
You can not use the same password: No puedes usar la misma contraseña
|
||||
Verification code: Código de verificación
|
||||
Password updated!: ¡Contraseña actualizada!
|
||||
Password requirements: >
|
||||
La contraseña debe tener al menos {{ length }} caracteres de longitud,
|
||||
|
|
|
@ -9,6 +9,7 @@ import './login';
|
|||
import './outLayout';
|
||||
import './recover-password';
|
||||
import './reset-password';
|
||||
import './validate-email';
|
||||
import './change-password';
|
||||
import './module-card';
|
||||
import './module-main';
|
||||
|
|
|
@ -24,13 +24,23 @@ export default class Controller {
|
|||
this.loading = false;
|
||||
})
|
||||
.catch(req => {
|
||||
this.loading = false;
|
||||
this.password = '';
|
||||
this.focusUser();
|
||||
if (req?.data?.error?.code === 'REQUIRES_2FA') {
|
||||
this.outLayout.login = {
|
||||
user: this.user,
|
||||
password: this.password,
|
||||
remember: this.remember
|
||||
};
|
||||
this.$state.go('validate-email');
|
||||
return;
|
||||
}
|
||||
|
||||
const err = req.data?.error;
|
||||
if (err?.code == 'passExpired')
|
||||
this.$state.go('change-password', err.details.token);
|
||||
|
||||
this.loading = false;
|
||||
this.password = '';
|
||||
this.focusUser();
|
||||
throw req;
|
||||
});
|
||||
}
|
||||
|
@ -44,5 +54,8 @@ Controller.$inject = ['$scope', '$element', '$state', 'vnAuth'];
|
|||
|
||||
ngModule.vnComponent('vnLogin', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
controller: Controller,
|
||||
require: {
|
||||
outLayout: '^vnOutLayout'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<h5 class="vn-mb-md vn-mt-lg" translate>Enter verification code</h5>
|
||||
<span translate>Please enter the verification code that we have sent to your email address within 5 minutes</span>
|
||||
<vn-textfield label="Code" ng-model="$ctrl.code" type="text" vn-focus>
|
||||
</vn-textfield>
|
||||
<div class="footer">
|
||||
<vn-submit label="Validate" ng-click="$ctrl.submit()"></vn-submit>
|
||||
<div class="spinner-wrapper">
|
||||
<vn-spinner enable="$ctrl.loading"></vn-spinner>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,43 @@
|
|||
import ngModule from '../../module';
|
||||
import './style.scss';
|
||||
|
||||
export default class Controller {
|
||||
constructor($scope, $element, vnAuth, $state) {
|
||||
Object.assign(this, {
|
||||
$scope,
|
||||
$element,
|
||||
vnAuth,
|
||||
user: localStorage.getItem('lastUser'),
|
||||
remember: true,
|
||||
$state
|
||||
});
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.loginData = this.outLayout.login;
|
||||
if (!this.loginData)
|
||||
this.$state.go('login');
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.loading = true;
|
||||
this.vnAuth.validateCode(this.loginData.user, this.loginData.password, this.code, this.loginData.remember)
|
||||
.then(() => {
|
||||
localStorage.setItem('lastUser', this.user);
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
Controller.$inject = ['$scope', '$element', 'vnAuth', '$state'];
|
||||
|
||||
ngModule.vnComponent('vnValidateEmail', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller,
|
||||
require: {
|
||||
outLayout: '^vnOutLayout'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
Validate email auth: Autenticar email
|
||||
Enter verification code: Introduce código de verificación
|
||||
Code: Código
|
||||
Please enter the verification code that we have sent to your email address within 5 minutes: Por favor, introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos
|
||||
Validate: Validar
|
|
@ -0,0 +1,24 @@
|
|||
@import "variables";
|
||||
|
||||
vn-validate-email {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,9 +37,15 @@ function config($stateProvider, $urlRouterProvider) {
|
|||
description: 'Reset password',
|
||||
template: '<vn-reset-password></vn-reset-password>'
|
||||
})
|
||||
.state('validate-email', {
|
||||
parent: 'outLayout',
|
||||
url: '/validate-email',
|
||||
description: 'Validate email auth',
|
||||
template: '<vn-validate-email></vn-validate-email>'
|
||||
})
|
||||
.state('change-password', {
|
||||
parent: 'outLayout',
|
||||
url: '/change-password?id&userId',
|
||||
url: '/change-password?id&userId&twoFactor',
|
||||
description: 'Change password',
|
||||
template: '<vn-change-password></vn-change-password>'
|
||||
})
|
||||
|
|
|
@ -39,7 +39,7 @@ module.exports = Self => {
|
|||
return [html, 'text/html', `filename=${fileName}.pdf"`];
|
||||
};
|
||||
|
||||
Self.sendTemplate = async function(ctx, templateName) {
|
||||
Self.sendTemplate = async function(ctx, templateName, force) {
|
||||
const args = Object.assign({}, ctx.args);
|
||||
const params = {
|
||||
recipient: args.recipient,
|
||||
|
@ -52,6 +52,6 @@ module.exports = Self => {
|
|||
|
||||
const email = new Email(templateName, params);
|
||||
|
||||
return email.send();
|
||||
return email.send({force: force});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -147,14 +147,12 @@
|
|||
"Receipt's bank was not found": "Receipt's bank was not found",
|
||||
"This receipt was not compensated": "This receipt was not compensated",
|
||||
"Client's email was not found": "Client's email was not found",
|
||||
"Tickets with associated refunds": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº {{id}}",
|
||||
"Tickets with associated refunds": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº %d",
|
||||
"It is not possible to modify tracked sales": "It is not possible to modify tracked sales",
|
||||
"It is not possible to modify sales that their articles are from Floramondo": "It is not possible to modify sales that their articles are from Floramondo",
|
||||
"It is not possible to modify cloned sales": "It is not possible to modify cloned sales",
|
||||
"Valid priorities: 1,2,3": "Valid priorities: 1,2,3",
|
||||
"Warehouse inventory not set": "Almacén inventario no está establecido",
|
||||
"Component cost not set": "Componente coste no está estabecido",
|
||||
"Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2",
|
||||
"Description cannot be blank": "Description cannot be blank",
|
||||
"company": "Company",
|
||||
"country": "Country",
|
||||
|
@ -178,5 +176,7 @@
|
|||
"Failed to upload delivery note": "Error to upload delivery note {{id}}",
|
||||
"Mail not sent": "There has been an error sending the invoice to the client [{{clientId}}]({{{clientUrl}}}), please check the email address",
|
||||
"The renew period has not been exceeded": "The renew period has not been exceeded",
|
||||
"You can not use the same password": "You can not use the same password",
|
||||
"Valid priorities": "Valid priorities: %d",
|
||||
"Negative basis of tickets": "Negative basis of tickets: {{ticketsIds}}"
|
||||
}
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"Invoice date can't be less than max date": "La fecha de factura no puede ser inferior a la fecha límite",
|
||||
"Warehouse inventory not set": "El almacén inventario no está establecido",
|
||||
"This locker has already been assigned": "Esta taquilla ya ha sido asignada",
|
||||
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}",
|
||||
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº %d",
|
||||
"Not exist this branch": "La rama no existe",
|
||||
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
|
||||
"Collection does not exist": "La colección no existe",
|
||||
|
@ -276,6 +276,8 @@
|
|||
"Insert a date range": "Inserte un rango de fechas",
|
||||
"Added observation": "{{user}} añadió esta observacion: {{text}}",
|
||||
"Comment added to client": "Observación añadida al cliente {{clientFk}}",
|
||||
"Invalid auth code": "Código de verificación incorrecto",
|
||||
"Invalid or expired verification code": "Código de verificación incorrecto o expirado",
|
||||
"Cannot create a new claimBeginning from a different ticket": "No se puede crear una línea de reclamación de un ticket diferente al origen",
|
||||
"company": "Compañía",
|
||||
"country": "País",
|
||||
|
@ -293,10 +295,15 @@
|
|||
"Invalid NIF for VIES": "Invalid NIF for VIES",
|
||||
"Ticket does not exist": "Este ticket no existe",
|
||||
"Ticket is already signed": "Este ticket ya ha sido firmado",
|
||||
"Authentication failed": "Autenticación fallida",
|
||||
"You can't use the same password": "No puedes usar la misma contraseña",
|
||||
"You can only add negative amounts in refund tickets": "Solo se puede añadir cantidades negativas en tickets abono",
|
||||
"Fecha fuera de rango": "Fecha fuera de rango",
|
||||
"Error while generating PDF": "Error al generar PDF",
|
||||
"Error when sending mail to client": "Error al enviar el correo al cliente",
|
||||
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
|
||||
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
|
||||
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado",
|
||||
"Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}"
|
||||
"Valid priorities": "Prioridades válidas: %d",
|
||||
"Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}",
|
||||
"You cannot assign/remove an alias that you are not assigned to": "No puede asignar/eliminar un alias que no tenga asignado"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = class ForbiddenError extends Error {
|
||||
constructor(message, code, ...translateArgs) {
|
||||
super(message);
|
||||
this.name = 'ForbiddenError';
|
||||
this.statusCode = 403;
|
||||
this.code = code;
|
||||
this.translateArgs = translateArgs;
|
||||
}
|
||||
};
|
|
@ -1,15 +1,12 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('changePassword', {
|
||||
Self.remoteMethodCtx('changePassword', {
|
||||
description: 'Changes the user password',
|
||||
accessType: 'WRITE',
|
||||
accessScopes: ['changePassword'],
|
||||
accepts: [
|
||||
{
|
||||
arg: 'id',
|
||||
type: 'number',
|
||||
description: 'The user id',
|
||||
http: {source: 'path'}
|
||||
}, {
|
||||
arg: 'oldPassword',
|
||||
type: 'string',
|
||||
description: 'The old password',
|
||||
|
@ -19,15 +16,35 @@ module.exports = Self => {
|
|||
type: 'string',
|
||||
description: 'The new password',
|
||||
required: true
|
||||
}, {
|
||||
arg: 'code',
|
||||
type: 'string',
|
||||
description: 'The 2FA code'
|
||||
}
|
||||
],
|
||||
http: {
|
||||
path: `/:id/changePassword`,
|
||||
path: `/change-password`,
|
||||
verb: 'PATCH'
|
||||
}
|
||||
});
|
||||
|
||||
Self.changePassword = async function(id, oldPassword, newPassword) {
|
||||
await Self.app.models.VnUser.changePassword(id, oldPassword, newPassword);
|
||||
Self.changePassword = async function(ctx, oldPassword, newPassword, code, options) {
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
|
||||
const myOptions = {};
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const {VnUser} = Self.app.models;
|
||||
const user = await VnUser.findById(userId, {fields: ['name', 'twoFactor']}, myOptions);
|
||||
await user.hasPassword(oldPassword);
|
||||
|
||||
if (oldPassword == newPassword)
|
||||
throw new UserError(`You can not use the same password`);
|
||||
|
||||
if (user.twoFactor)
|
||||
await VnUser.validateCode(user.name, code, myOptions);
|
||||
|
||||
await VnUser.changePassword(userId, oldPassword, newPassword, myOptions);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethod('login', {
|
||||
Self.remoteMethodCtx('login', {
|
||||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{
|
||||
|
@ -23,5 +23,5 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.login = async(user, password) => Self.app.models.VnUser.signIn(user, password);
|
||||
Self.login = async(ctx, user, password, options) => Self.app.models.VnUser.signIn(ctx, user, password, options);
|
||||
};
|
||||
|
|
|
@ -21,7 +21,8 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.setPassword = async function(id, newPassword) {
|
||||
await Self.app.models.VnUser.setPassword(id, newPassword);
|
||||
Self.setPassword = async function(id, newPassword, options) {
|
||||
options = typeof options == 'object' ? options : {};
|
||||
await Self.app.models.VnUser.setPassword(id, newPassword, options);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,22 +1,99 @@
|
|||
const {models} = require('vn-loopback/server/server');
|
||||
|
||||
describe('account changePassword()', () => {
|
||||
it('should throw an error when old password is wrong', async() => {
|
||||
let error;
|
||||
try {
|
||||
await models.Account.changePassword(1, 'wrongPassword', 'nightmare.9999');
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
const ctx = {req: {accessToken: {userId: 70}}};
|
||||
const unauthCtx = {
|
||||
req: {
|
||||
headers: {},
|
||||
connection: {
|
||||
remoteAddress: '127.0.0.1'
|
||||
},
|
||||
getLocale: () => 'en'
|
||||
},
|
||||
args: {}
|
||||
};
|
||||
describe('Without 2FA', () => {
|
||||
it('should throw an error when old password is wrong', async() => {
|
||||
const tx = await models.Account.beginTransaction({});
|
||||
|
||||
expect(error).toContain('Invalid current password');
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
await models.Account.changePassword(ctx, 'wrongPassword', 'nightmare.9999', null, options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e.message;
|
||||
}
|
||||
|
||||
expect(error).toContain('Invalid current password');
|
||||
});
|
||||
|
||||
it('should throw an error when old and new password are the same', async() => {
|
||||
const tx = await models.Account.beginTransaction({});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', null, options);
|
||||
await models.Account.changePassword(ctx, 'nightmare.9999', 'nightmare.9999', null, options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e.message;
|
||||
}
|
||||
|
||||
expect(error).toContain('You can not use the same password');
|
||||
});
|
||||
|
||||
it('should change password', async() => {
|
||||
const tx = await models.Account.beginTransaction({});
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', null, options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
|
||||
expect(e).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should change password', async() => {
|
||||
try {
|
||||
await models.Account.changePassword(70, 'nightmare', 'nightmare.9999');
|
||||
} catch (e) {
|
||||
expect(e).toBeUndefined();
|
||||
}
|
||||
describe('With 2FA', () => {
|
||||
it('should change password when code is correct', async() => {
|
||||
const tx = await models.Account.beginTransaction({});
|
||||
const yesterday = Date.vnNew();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const options = {transaction: tx};
|
||||
try {
|
||||
await models.VnUser.updateAll(
|
||||
{id: 70},
|
||||
{
|
||||
twoFactor: 'email',
|
||||
passExpired: yesterday
|
||||
}
|
||||
, options);
|
||||
await models.VnUser.signIn(unauthCtx, 'trainee', 'nightmare', options);
|
||||
} catch (e) {
|
||||
if (e.message != 'Pass expired')
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
const authCode = await models.AuthCode.findOne({where: {userFk: 70}}, options);
|
||||
await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', authCode.code, options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
|
||||
expect(e).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,8 +8,18 @@ describe('Account setPassword()', () => {
|
|||
});
|
||||
|
||||
it('should update password when it passes requirements', async() => {
|
||||
let req = models.Account.setPassword(1, 'Very$ecurePa22.');
|
||||
const tx = await models.Account.beginTransaction({});
|
||||
|
||||
await expectAsync(req).toBeResolved();
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
await models.Account.setPassword(1, 'Very$ecurePa22.', options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,6 +37,13 @@
|
|||
"principalType": "ROLE",
|
||||
"principalId": "$authenticated",
|
||||
"permission": "ALLOW"
|
||||
},
|
||||
{
|
||||
"property": "changePassword",
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.observe('before save', async ctx => {
|
||||
const changes = ctx.currentInstance || ctx.instance;
|
||||
|
||||
await Self.hasGrant(ctx, changes.mailAlias);
|
||||
});
|
||||
|
||||
Self.observe('before delete', async ctx => {
|
||||
const mailAliasAccount = await Self.findById(ctx.where.id);
|
||||
|
||||
await Self.hasGrant(ctx, mailAliasAccount.mailAlias);
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks if current user has
|
||||
* grant to add/remove alias
|
||||
*
|
||||
* @param {Object} ctx - Request context
|
||||
* @param {Interger} mailAlias - mailAlias id
|
||||
* @return {Boolean} True for user with grant
|
||||
*/
|
||||
Self.hasGrant = async function(ctx, mailAlias) {
|
||||
const models = Self.app.models;
|
||||
const accessToken = {req: {accessToken: ctx.options.accessToken}};
|
||||
const userId = accessToken.req.accessToken.userId;
|
||||
|
||||
const canEditAlias = await models.ACL.checkAccessAcl(accessToken, 'MailAliasAccount', 'canEditAlias', 'WRITE');
|
||||
if (canEditAlias) return true;
|
||||
|
||||
const user = await models.VnUser.findById(userId, {fields: ['hasGrant']});
|
||||
if (!user.hasGrant)
|
||||
throw new UserError(`You don't have grant privilege`);
|
||||
|
||||
const account = await models.Account.findById(userId, {
|
||||
fields: ['id'],
|
||||
include: {
|
||||
relation: 'aliases',
|
||||
scope: {
|
||||
fields: ['mailAlias']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const aliases = account.aliases().map(alias => alias.mailAlias);
|
||||
|
||||
const hasAlias = aliases.includes(mailAlias);
|
||||
if (!hasAlias)
|
||||
throw new UserError(`You cannot assign/remove an alias that you are not assigned to`);
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
|
@ -17,9 +17,7 @@
|
|||
<vn-icon-button
|
||||
icon="delete"
|
||||
translate-attr="{title: 'Unsubscribe'}"
|
||||
ng-click="removeConfirm.show(row)"
|
||||
vn-acl="itManagement"
|
||||
vn-acl-action="remove">
|
||||
ng-click="removeConfirm.show(row)">
|
||||
</vn-icon-button>
|
||||
</vn-item-section>
|
||||
</vn-item>
|
||||
|
@ -32,9 +30,7 @@
|
|||
translate-attr="{title: 'Add'}"
|
||||
vn-bind="+"
|
||||
ng-click="$ctrl.onAddClick()"
|
||||
fixed-bottom-right
|
||||
vn-acl="itManagement"
|
||||
vn-acl-action="remove">
|
||||
fixed-bottom-right>
|
||||
</vn-float-button>
|
||||
<vn-dialog
|
||||
vn-id="dialog"
|
||||
|
|
|
@ -28,7 +28,7 @@ module.exports = Self => {
|
|||
const isAccount = await models.Account.findById(id);
|
||||
|
||||
if (isClient && !isAccount)
|
||||
await models.Account.setPassword(id, newPassword);
|
||||
await models.VnUser.setPassword(id, newPassword);
|
||||
else
|
||||
throw new UserError(`Modifiable password only via recovery or by an administrator`);
|
||||
};
|
||||
|
|
|
@ -60,6 +60,7 @@ module.exports = Self => {
|
|||
DISTINCT c.id clientFk,
|
||||
c.name clientName,
|
||||
c.salesPersonFk,
|
||||
c.businessTypeFk,
|
||||
u.name salesPersonName,
|
||||
d.amount,
|
||||
co.created,
|
||||
|
|
|
@ -282,7 +282,7 @@ module.exports = Self => {
|
|||
await Self.changeCredit(ctx, finalState, changes);
|
||||
|
||||
// Credit management changes
|
||||
if (orgData?.rating != changes.rating || orgData?.recommendedCredit != changes.recommendedCredit)
|
||||
if (changes?.rating || changes?.recommendedCredit)
|
||||
await Self.changeCreditManagement(ctx, finalState, changes);
|
||||
|
||||
const oldInstance = {};
|
||||
|
@ -406,17 +406,27 @@ module.exports = Self => {
|
|||
const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE');
|
||||
if (!canEditCredit) {
|
||||
const lastCredit = await models.ClientCredit.findOne({
|
||||
field: ['workerFk', 'amount'],
|
||||
where: {
|
||||
clientFk: finalState.id
|
||||
},
|
||||
order: 'id DESC'
|
||||
}, ctx.options);
|
||||
|
||||
const lastAmount = lastCredit && lastCredit.amount;
|
||||
const lastCreditIsNotEditable = !await models.ACL.checkAccessAcl(accessToken, 'Client', 'isNotEditableCredit', 'WRITE');
|
||||
if (lastCredit && lastCredit.amount == 0) {
|
||||
const zeroCreditEditor =
|
||||
await models.ACL.checkAccessAcl(accessToken, 'Client', 'zeroCreditEditor', 'WRITE');
|
||||
const lastCreditIsNotEditable =
|
||||
await models.ACL.checkAccessAcl(
|
||||
{req: {accessToken: {userId: lastCredit.workerFk}}},
|
||||
'Client',
|
||||
'zeroCreditEditor',
|
||||
'WRITE'
|
||||
);
|
||||
|
||||
if (lastAmount == 0 && lastCreditIsNotEditable)
|
||||
throw new UserError(`You can't change the credit set to zero from a financialBoss`);
|
||||
if (lastCreditIsNotEditable && !zeroCreditEditor)
|
||||
throw new UserError(`You can't change the credit set to zero from a financialBoss`);
|
||||
}
|
||||
|
||||
const creditLimits = await models.ClientCreditLimit.find({
|
||||
fields: ['roleFk'],
|
||||
|
|
|
@ -60,22 +60,22 @@ describe('Client Model', () => {
|
|||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
const context = {options};
|
||||
const ctx = {options};
|
||||
|
||||
// Set credit to zero by a financialBoss
|
||||
const financialBoss = await models.VnUser.findOne({
|
||||
where: {name: 'financialBoss'}
|
||||
}, options);
|
||||
context.options.accessToken = {userId: financialBoss.id};
|
||||
ctx.options.accessToken = {userId: financialBoss.id};
|
||||
|
||||
await models.Client.changeCredit(context, instance, {credit: 0});
|
||||
await models.Client.changeCredit(ctx, instance, {credit: 0});
|
||||
|
||||
const salesAssistant = await models.VnUser.findOne({
|
||||
where: {name: 'salesAssistant'}
|
||||
}, options);
|
||||
context.options.accessToken = {userId: salesAssistant.id};
|
||||
ctx.options.accessToken = {userId: salesAssistant.id};
|
||||
|
||||
await models.Client.changeCredit(context, instance, {credit: 300});
|
||||
await models.Client.changeCredit(ctx, instance, {credit: 300});
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
|
@ -93,14 +93,14 @@ describe('Client Model', () => {
|
|||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
const context = {options};
|
||||
const ctx = {options};
|
||||
|
||||
const salesAssistant = await models.VnUser.findOne({
|
||||
where: {name: 'salesAssistant'}
|
||||
}, options);
|
||||
context.options.accessToken = {userId: salesAssistant.id};
|
||||
ctx.options.accessToken = {userId: salesAssistant.id};
|
||||
|
||||
await models.Client.changeCredit(context, instance, {credit: 99999});
|
||||
await models.Client.changeCredit(ctx, instance, {credit: 99999});
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
filter="::$ctrl.filter"
|
||||
limit="20"
|
||||
order="amount DESC"
|
||||
data="defaulters"
|
||||
data="$ctrl.defaulters"
|
||||
on-data-change="$ctrl.reCheck()"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
<div class="vn-pa-md">
|
||||
<vn-button
|
||||
ng-show="$ctrl.checked.length > 0"
|
||||
disabled="$ctrl.checked.length == 0"
|
||||
ng-click="notesDialog.show()"
|
||||
name="notesDialog"
|
||||
vn-tooltip="Add observation"
|
||||
|
@ -54,6 +54,9 @@
|
|||
<th field="clientFk">
|
||||
<span translate>Client</span>
|
||||
</th>
|
||||
<th>
|
||||
<span translate>Es trabajador</span>
|
||||
</th>
|
||||
<th field="salesPersonFk">
|
||||
<span translate>Comercial</span>
|
||||
</th>
|
||||
|
@ -94,7 +97,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="defaulter in defaulters">
|
||||
<tr ng-repeat="defaulter in $ctrl.defaulters">
|
||||
<td shrink>
|
||||
<vn-check
|
||||
ng-model="defaulter.checked"
|
||||
|
@ -110,6 +113,12 @@
|
|||
{{::defaulter.clientName}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<vn-check
|
||||
ng-model="defaulter.isWorker"
|
||||
disabled="true">
|
||||
</vn-check>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
title="{{::defaulter.salesPersonName}}"
|
||||
|
|
|
@ -6,6 +6,7 @@ export default class Controller extends Section {
|
|||
constructor($element, $) {
|
||||
super($element, $);
|
||||
this.defaulter = {};
|
||||
this.defaulters = [];
|
||||
this.checkedDefaulers = [];
|
||||
|
||||
this.smartTableOptions = {
|
||||
|
@ -70,6 +71,18 @@ export default class Controller extends Section {
|
|||
this.getBalanceDueTotal();
|
||||
}
|
||||
|
||||
set defaulters(value) {
|
||||
if (!value || !value.length) return;
|
||||
for (let defaulter of value)
|
||||
defaulter.isWorker = defaulter.businessTypeFk === 'worker';
|
||||
|
||||
this._defaulters = value;
|
||||
}
|
||||
|
||||
get defaulters() {
|
||||
return this._defaulters;
|
||||
}
|
||||
|
||||
get checked() {
|
||||
const clients = this.$.model.data || [];
|
||||
const checkedLines = [];
|
||||
|
|
|
@ -17,6 +17,7 @@ describe('InvoiceOut refund()', () => {
|
|||
const options = {transaction: tx};
|
||||
|
||||
try {
|
||||
await models.TicketRefund.destroyAll(null, options);
|
||||
const result = await models.InvoiceOut.refund(ctx, 'T1111111', withWarehouse, options);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
let UserError = require('vn-loopback/util/user-error');
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('new', {
|
||||
|
@ -49,7 +49,7 @@ module.exports = Self => {
|
|||
try {
|
||||
const itemConfig = await models.ItemConfig.findOne({fields: ['validPriorities']}, myOptions);
|
||||
if (!itemConfig.validPriorities.includes(params.priority))
|
||||
throw new UserError(`Valid priorities: ${[...itemConfig.validPriorities]}`);
|
||||
throw new UserError('Valid priorities', 'VALID_PRIORITIES', [...itemConfig.validPriorities]);
|
||||
|
||||
const provisionalName = params.provisionalName;
|
||||
delete params.provisionalName;
|
||||
|
|
|
@ -64,7 +64,7 @@ describe('item getBalance()', () => {
|
|||
const secondItemBalance = await models.Item.getBalance(ctx, secondFilter, options);
|
||||
|
||||
expect(firstItemBalance[9].claimFk).toEqual(null);
|
||||
expect(secondItemBalance[5].claimFk).toEqual(2);
|
||||
expect(secondItemBalance[4].claimFk).toEqual(2);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
|
|
|
@ -42,8 +42,9 @@ module.exports = Self => {
|
|||
|
||||
promises.push(Self.app.models.ItemTaxCountry.updateAll(
|
||||
{id: tax.id},
|
||||
{taxClassFk: tax.taxClassFk}
|
||||
), myOptions);
|
||||
{taxClassFk: tax.taxClassFk},
|
||||
myOptions
|
||||
));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ module.exports = Self => {
|
|||
const changes = ctx.data || ctx.instance;
|
||||
const orgData = ctx.currentInstance;
|
||||
const loopBackContext = LoopBackContext.getCurrentContext();
|
||||
const accessToken = {req: loopBackContext.active.accessToken};
|
||||
const accessToken = {req: loopBackContext.active};
|
||||
|
||||
const editPayMethodCheck =
|
||||
await Self.app.models.ACL.checkAccessAcl(accessToken, 'Supplier', 'editPayMethodCheck', 'WRITE');
|
||||
|
|
|
@ -110,4 +110,53 @@ describe('sale updateQuantity()', () => {
|
|||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw an error if the quantity is negative and it is not a refund ticket', async() => {
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 1},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
const saleId = 17;
|
||||
const newQuantity = -10;
|
||||
|
||||
const tx = await models.Sale.beginTransaction({});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toEqual(new Error('You can only add negative amounts in refund tickets'));
|
||||
});
|
||||
|
||||
it('should update a negative quantity when is a ticket refund', async() => {
|
||||
const tx = await models.Sale.beginTransaction({});
|
||||
const saleId = 13;
|
||||
const newQuantity = -10;
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
|
||||
|
||||
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
|
||||
|
||||
expect(modifiedLine.quantity).toEqual(newQuantity);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,6 +68,13 @@ module.exports = Self => {
|
|||
if (newQuantity > sale.quantity && !isRoleAdvanced)
|
||||
throw new UserError('The new quantity should be smaller than the old one');
|
||||
|
||||
const ticketRefund = await models.TicketRefund.findOne({
|
||||
where: {refundTicketFk: sale.ticketFk},
|
||||
fields: ['id']}
|
||||
, myOptions);
|
||||
if (newQuantity < 0 && !ticketRefund)
|
||||
throw new UserError('You can only add negative amounts in refund tickets');
|
||||
|
||||
const oldQuantity = sale.quantity;
|
||||
const result = await sale.updateAttributes({quantity: newQuantity}, myOptions);
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ module.exports = Self => {
|
|||
fields: ['id']}
|
||||
, myOptions);
|
||||
if (ticketRefunds.length > 0)
|
||||
throw new UserError($t('Tickets with associated refunds', {id: ticketRefunds[0].id}));
|
||||
throw new UserError('Tickets with associated refunds', 'TICKET_REFUND', ticketRefunds[0].id);
|
||||
|
||||
// Check if has sales with shelving
|
||||
const canDeleteTicketWithPartPrepared =
|
||||
|
|
|
@ -141,6 +141,7 @@ describe('ticket filter()', () => {
|
|||
});
|
||||
|
||||
it('should return the tickets that are not pending', async() => {
|
||||
pending('#6010 test intermitente');
|
||||
const tx = await models.Ticket.beginTransaction({});
|
||||
|
||||
try {
|
||||
|
|
|
@ -38,10 +38,12 @@ class Controller extends SearchPanel {
|
|||
|
||||
applyFilters(param) {
|
||||
if (typeof this.filter.scopeDays === 'number') {
|
||||
const shippedFrom = Date.vnNew();
|
||||
const today = Date.vnNew();
|
||||
const shippedFrom = new Date(today.getTime());
|
||||
shippedFrom.setDate(today.getDate() - 30);
|
||||
shippedFrom.setHours(0, 0, 0, 0);
|
||||
|
||||
const shippedTo = new Date(shippedFrom.getTime());
|
||||
const shippedTo = new Date(today.getTime());
|
||||
shippedTo.setDate(shippedTo.getDate() + this.filter.scopeDays);
|
||||
shippedTo.setHours(23, 59, 59, 999);
|
||||
Object.assign(this.filter, {shippedFrom, shippedTo});
|
||||
|
|
|
@ -33,7 +33,6 @@ module.exports = Self => {
|
|||
map.set(node.parentFk, []);
|
||||
map.get(node.parentFk).push(node);
|
||||
}
|
||||
|
||||
function setLeaves(nodes) {
|
||||
if (!nodes) return;
|
||||
for (let node of nodes) {
|
||||
|
@ -43,6 +42,7 @@ module.exports = Self => {
|
|||
}
|
||||
|
||||
let leaves = map.get(parentId);
|
||||
|
||||
setLeaves(leaves);
|
||||
|
||||
return leaves || [];
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
|
||||
describe('department getLeaves()', () => {
|
||||
const ctx = {req: {accessToken: {userId: 9}}};
|
||||
it('should return the department and the childs containing the search value', async() => {
|
||||
let result = await models.Department.getLeaves(ctx, null, 'INFORMATICA');
|
||||
|
||||
expect(result.length).toEqual(1);
|
||||
});
|
||||
});
|
|
@ -171,7 +171,7 @@ module.exports = Self => {
|
|||
throw new UserError(`That payment method requires an IBAN`);
|
||||
|
||||
await models.Worker.rawSql(
|
||||
'CALL vn.clientCreate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
'CALL vn.client_create(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
args.firstName,
|
||||
args.lastNames,
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"id": true,
|
||||
"type": "number"
|
||||
"description": "Identifier"
|
||||
},
|
||||
"code": {
|
||||
"type": "string"
|
||||
|
@ -37,6 +38,30 @@
|
|||
},
|
||||
"hasToMistake": {
|
||||
"type": "number"
|
||||
},
|
||||
"isTeleworking": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasToRefill": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasToSendMail": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isProduction": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"client": {
|
||||
"type": "belongsTo",
|
||||
"model": "Client",
|
||||
"foreignKey": "clientFk"
|
||||
},
|
||||
"worker": {
|
||||
"type": "belongsTo",
|
||||
"model": "Worker",
|
||||
"foreignKey": "workerFk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,9 +43,6 @@
|
|||
"SSN": {
|
||||
"type" : "string"
|
||||
},
|
||||
"labelerFk": {
|
||||
"type" : "number"
|
||||
},
|
||||
"mobileExtension": {
|
||||
"type" : "number"
|
||||
},
|
||||
|
@ -60,7 +57,7 @@
|
|||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "VnUser",
|
||||
"foreignKey": "userFk"
|
||||
"foreignKey": "id"
|
||||
},
|
||||
"boss": {
|
||||
"type": "belongsTo",
|
||||
|
@ -86,11 +83,6 @@
|
|||
"type": "hasMany",
|
||||
"model": "WorkerTeamCollegues",
|
||||
"foreignKey": "workerFk"
|
||||
},
|
||||
"sector": {
|
||||
"type": "belongsTo",
|
||||
"model": "Sector",
|
||||
"foreignKey": "sectorFk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,22 +3,30 @@
|
|||
data="absenceTypes"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<div class="vn-w-lg">
|
||||
<vn-card class="vn-pa-sm calendars">
|
||||
<vn-icon ng-if="::$ctrl.isSubordinate" icon="info" color-marginal
|
||||
vn-tooltip="To start adding absences, click an absence type from the right menu and then on the day you want to add an absence">
|
||||
</vn-icon>
|
||||
<vn-calendar
|
||||
ng-repeat="month in $ctrl.months"
|
||||
data="$ctrl.events"
|
||||
default-date="month"
|
||||
format-day="$ctrl.formatDay($day, $element)"
|
||||
display-controls="false"
|
||||
hide-contiguous="true"
|
||||
hide-year="true"
|
||||
on-selection="$ctrl.onSelection($event, $days)">
|
||||
</vn-calendar>
|
||||
</vn-card>
|
||||
<div ng-if="$ctrl.worker.hasWorkCenter">
|
||||
<div class="vn-w-lg">
|
||||
<vn-card class="vn-pa-sm calendars">
|
||||
<vn-icon ng-if="::$ctrl.isSubordinate" icon="info" color-marginal
|
||||
vn-tooltip="To start adding absences, click an absence type from the right menu and then on the day you want to add an absence">
|
||||
</vn-icon>
|
||||
<vn-calendar
|
||||
ng-repeat="month in $ctrl.months"
|
||||
data="$ctrl.events"
|
||||
default-date="month"
|
||||
format-day="$ctrl.formatDay($day, $element)"
|
||||
display-controls="false"
|
||||
hide-contiguous="true"
|
||||
hide-year="true"
|
||||
on-selection="$ctrl.onSelection($event, $days)">
|
||||
</vn-calendar>
|
||||
</vn-card>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ng-if="!$ctrl.worker.hasWorkCenter"
|
||||
class="bg-title"
|
||||
translate>
|
||||
Autonomous worker
|
||||
</div>
|
||||
<vn-side-menu side="right">
|
||||
<div class="vn-pa-md">
|
||||
|
@ -63,7 +71,6 @@
|
|||
ng-model="$ctrl.businessId"
|
||||
search-function="{businessFk: $search}"
|
||||
value-field="businessFk"
|
||||
show-field="businessFk"
|
||||
order="businessFk DESC"
|
||||
limit="5">
|
||||
<tpl-item>
|
||||
|
@ -103,3 +110,4 @@
|
|||
message="This item will be deleted"
|
||||
question="Are you sure you want to continue?">
|
||||
</vn-confirm>
|
||||
|
||||
|
|
|
@ -64,8 +64,7 @@ class Controller extends Section {
|
|||
|
||||
set worker(value) {
|
||||
this._worker = value;
|
||||
|
||||
if (value) {
|
||||
if (value && value.hasWorkCenter) {
|
||||
this.getIsSubordinate();
|
||||
this.getActiveContract();
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ describe('Worker', () => {
|
|||
let yesterday = new Date(today.getTime());
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
controller.worker = {id: 1107};
|
||||
controller.worker = {id: 1107, hasWorkCenter: true};
|
||||
|
||||
expect(controller.getIsSubordinate).toHaveBeenCalledWith();
|
||||
expect(controller.getActiveContract).toHaveBeenCalledWith();
|
||||
|
|
|
@ -11,4 +11,5 @@ Choose an absence type from the right menu: Elige un tipo de ausencia desde el m
|
|||
To start adding absences, click an absence type from the right menu and then on the day you want to add an absence: Para empezar a añadir ausencias, haz clic en un tipo de ausencia desde el menu de la derecha y después en el día que quieres añadir la ausencia
|
||||
You can just add absences within the current year: Solo puedes añadir ausencias dentro del año actual
|
||||
Current day: Día actual
|
||||
Paid holidays: Vacaciones pagadas
|
||||
Paid holidays: Vacaciones pagadas
|
||||
Autonomous worker: Trabajador autónomo
|
||||
|
|
|
@ -33,7 +33,12 @@ class Controller extends ModuleCard {
|
|||
};
|
||||
|
||||
this.$http.get(`Workers/${this.$params.id}`, {filter})
|
||||
.then(res => this.worker = res.data);
|
||||
.then(res => this.worker = res.data)
|
||||
.then(() =>
|
||||
this.$http.get(`Workers/${this.$params.id}/activeContract`)
|
||||
.then(res => {
|
||||
if (res.data) this.worker.hasWorkCenter = res.data.workCenterFk;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
<vn-watcher
|
||||
vn-id="watcher"
|
||||
data="$ctrl.department"
|
||||
form="form"
|
||||
url="Departments">
|
||||
</vn-watcher>
|
||||
<vn-portal slot="topbar">
|
||||
<vn-searchbar
|
||||
vn-focus
|
||||
info="Search departments by id, name or code"
|
||||
on-search="$ctrl.onSearch($params)"
|
||||
base-state="worker.department">
|
||||
</vn-searchbar>
|
||||
</vn-portal>
|
||||
<form name="form" ng-submit="watcher.submit()" class="vn-w-md">
|
||||
<vn-card class="vn-pa-lg">
|
||||
<vn-vertical>
|
||||
<vn-horizontal>
|
||||
<vn-textfield
|
||||
vn-one
|
||||
label="Name"
|
||||
ng-model="$ctrl.department.name"
|
||||
vn-name="Name">
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
vn-one
|
||||
label="Code"
|
||||
ng-model="$ctrl.department.code"
|
||||
vn-name="Code">
|
||||
</vn-textfield>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-textfield
|
||||
vn-one
|
||||
label="Chat"
|
||||
ng-model="$ctrl.department.chatName"
|
||||
vn-name="Chat">
|
||||
</vn-textfield>
|
||||
<vn-textfield type="Email"
|
||||
vn-one
|
||||
label="Email"
|
||||
ng-model="$ctrl.department.notificationEmail"
|
||||
vn-name="Email">
|
||||
</vn-textfield>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-autocomplete
|
||||
ng-model="$ctrl.department.workerFk"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
label="Boss department">
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
ng-model="$ctrl.department.clientFk"
|
||||
url="Clients/"
|
||||
show-field="name"
|
||||
search-function="{firstName: $search}"
|
||||
label="Self-consumption customer">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-check
|
||||
label="Telework"
|
||||
ng-model="$ctrl.department.isTeleworking">
|
||||
</vn-check>
|
||||
<vn-check
|
||||
label="Notify on errors"
|
||||
ng-model="$ctrl.department.hasToMistake">
|
||||
</vn-check>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-check
|
||||
label="worksInProduction"
|
||||
ng-model="$ctrl.department.isProduction">
|
||||
</vn-check>
|
||||
<vn-check
|
||||
label="Fill in days without physical check-ins"
|
||||
ng-model="$ctrl.department.hasToRefill">
|
||||
</vn-check>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-check
|
||||
label="Send check-ins by email"
|
||||
ng-model="$ctrl.department.hasToSendMail">
|
||||
</vn-check>
|
||||
</vn-horizontal>
|
||||
</vn-vertical>
|
||||
</vn-card>
|
||||
<vn-button-bar>
|
||||
<vn-submit
|
||||
disabled="!watcher.dataChanged()"
|
||||
label="Save">
|
||||
</vn-submit>
|
||||
<vn-button
|
||||
class="cancel"
|
||||
label="Undo changes"
|
||||
disabled="!watcher.dataChanged()"
|
||||
ng-click="watcher.loadOriginalData()">
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
</form>
|
|
@ -0,0 +1,10 @@
|
|||
import ngModule from '../../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
ngModule.vnComponent('vnWorkerDepartmentBasicData', {
|
||||
template: require('./index.html'),
|
||||
controller: Section,
|
||||
bindings: {
|
||||
department: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
Name: Nombre
|
||||
Code: Código
|
||||
Chat: Chat
|
||||
Email: Email
|
||||
Boss department: Jefe del departamento
|
||||
Self-consumption customer: Cliente autoconsumo
|
||||
Telecommutes: Teletrabaja
|
||||
Notify on errors: Notificar errores
|
||||
worksInProduction: Pertenece a producción
|
||||
Fill in days without physical check-ins: Completar días sin registros físicos
|
||||
Send check-ins by email: Enviar fichadas por email
|
||||
Save: Guardar
|
||||
Undo changes: Deshacer cambios
|
|
@ -0,0 +1,5 @@
|
|||
<vn-portal slot="menu">
|
||||
<vn-worker-department-descriptor department="$ctrl.department"></vn-worker-department-descriptor>
|
||||
<vn-left-menu source="department"></vn-left-menu>
|
||||
</vn-portal>
|
||||
<ui-view></ui-view>
|
|
@ -0,0 +1,39 @@
|
|||
import ngModule from '../../module';
|
||||
import ModuleCard from 'salix/components/module-card';
|
||||
|
||||
class Controller extends ModuleCard {
|
||||
reload() {
|
||||
const filter = {
|
||||
fields: ['id', 'name', 'code', 'workerFk', 'isProduction', 'chatName',
|
||||
'isTeleworking', 'notificationEmail', 'hasToRefill', 'hasToSendMail', 'hasToMistake', 'clientFk'],
|
||||
include: [
|
||||
{
|
||||
relation: 'client',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
}, {
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
fields: ['id', 'firstName', 'lastName'],
|
||||
include: {
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
this.$http.get(`Departments/${this.$params.id}`, {filter})
|
||||
.then(res => this.department = res.data);
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnWorkerDepartmentCard', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
<slot-descriptor>
|
||||
<vn-worker-department-descriptor>
|
||||
</vn-worker-department-descriptor>
|
||||
</slot-descriptor>
|
|
@ -0,0 +1,9 @@
|
|||
import ngModule from '../../module';
|
||||
import DescriptorPopover from 'salix/components/descriptor-popover';
|
||||
|
||||
class Controller extends DescriptorPopover {}
|
||||
|
||||
ngModule.vnComponent('vnWorkerDepartmentDescriptorPopover', {
|
||||
slotTemplate: require('./index.html'),
|
||||
controller: Controller
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
<vn-descriptor-content
|
||||
module="worker"
|
||||
description="$ctrl.department.name"
|
||||
base-state="worker.department">
|
||||
<slot-menu>
|
||||
<vn-item
|
||||
ng-click="deleteConfirmation.show()"
|
||||
name="deleteDepartment"
|
||||
translate>
|
||||
Delete
|
||||
</vn-item>
|
||||
</slot-menu>
|
||||
<slot-body>
|
||||
<div class="attributes">
|
||||
<vn-label-value
|
||||
label="Chat"
|
||||
value="{{$ctrl.department.chatName}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value
|
||||
label="Email"
|
||||
value="{{$ctrl.department.emailNotification}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value
|
||||
label="Self-consumption customer"
|
||||
value="{{$ctrl.department.client.name}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value
|
||||
label="Boss department"
|
||||
value="{{$ctrl.department.worker.firstName}}">
|
||||
</vn-label-value>
|
||||
</div>
|
||||
<div class="quicklinks">
|
||||
<div ng-transclude="btnOne">
|
||||
<vn-quick-link vn-anchor="{state: 'worker.index', params: {q: {departmentFk: $ctrl.$params.id} } }"
|
||||
vn-tooltip="Department workers"
|
||||
icon="icon-worker">
|
||||
</vn-quick-link>
|
||||
</div>
|
||||
</div>
|
||||
</slot-body>
|
||||
</vn-descriptor-content>
|
||||
<vn-popup vn-id="summary">
|
||||
<vn-worker-summary worker="$ctrl.worker"></vn-worker-summary>
|
||||
</vn-popup>
|
||||
<vn-confirm
|
||||
vn-id="deleteConfirmation"
|
||||
on-accept="$ctrl.deleteDepartment()"
|
||||
question="Are you sure you want to delete this department?">
|
||||
</vn-confirm>
|
|
@ -0,0 +1,45 @@
|
|||
import ngModule from '../../module';
|
||||
import Descriptor from 'salix/components/descriptor';
|
||||
|
||||
class Controller extends Descriptor {
|
||||
constructor($element, $, $rootScope) {
|
||||
super($element, $);
|
||||
this.$rootScope = $rootScope;
|
||||
}
|
||||
|
||||
get department() {
|
||||
return this.entity;
|
||||
}
|
||||
|
||||
set department(value) {
|
||||
this.entity = value;
|
||||
}
|
||||
|
||||
filterDepartments(department, event) {
|
||||
if (event.defaultPrevented) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.$state.go(`worker.index`,
|
||||
{q: JSON.stringify({departmentFk: department.id})});
|
||||
}
|
||||
|
||||
deleteDepartment() {
|
||||
return this.$http.delete(`Departments/${this.id}`)
|
||||
.then(() => {
|
||||
this.$state.go('worker.department');
|
||||
|
||||
this.vnApp.showSuccess(this.$t('Department deleted.'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Controller.$inject = ['$element', '$scope', '$rootScope'];
|
||||
|
||||
ngModule.vnComponent('vnWorkerDepartmentDescriptor', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller,
|
||||
bindings: {
|
||||
department: '<'
|
||||
}
|
||||
});
|
|
@ -1,83 +1,7 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
class Controller extends Section {
|
||||
$postLink() {
|
||||
this.$.treeview.fetch();
|
||||
}
|
||||
|
||||
onFetch(item) {
|
||||
const params = item ? {parentId: item.id} : null;
|
||||
return this.$.model.applyFilter({}, params).then(() => {
|
||||
return this.$.model.data;
|
||||
});
|
||||
}
|
||||
|
||||
onSort(a, b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
onDrop(dropped, dragged) {
|
||||
const params = dropped ? {parentId: dropped.id} : null;
|
||||
const query = `departments/${dragged.id}/moveChild`;
|
||||
this.$http.post(query, params).then(() => {
|
||||
this.$.treeview.move(dragged, dropped);
|
||||
});
|
||||
}
|
||||
|
||||
onCreate(parent) {
|
||||
this.newChild = {
|
||||
parent: parent,
|
||||
name: ''
|
||||
};
|
||||
|
||||
this.$.createNode.show();
|
||||
}
|
||||
|
||||
onCreateDialogOpen() {
|
||||
this.newChild.name = '';
|
||||
}
|
||||
|
||||
onCreateResponse() {
|
||||
try {
|
||||
if (!this.newChild.name)
|
||||
throw new Error(`Name can't be empty`);
|
||||
|
||||
const params = {name: this.newChild.name};
|
||||
const parent = this.newChild.parent;
|
||||
|
||||
if (parent && parent.id)
|
||||
params.parentId = parent.id;
|
||||
|
||||
const query = `departments/createChild`;
|
||||
this.$http.post(query, params).then(res => {
|
||||
const item = res.data;
|
||||
item.parent = parent;
|
||||
|
||||
this.$.treeview.create(item);
|
||||
});
|
||||
} catch (e) {
|
||||
this.vnApp.showError(this.$t(e.message));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onRemove(item) {
|
||||
this.removedChild = item;
|
||||
this.$.deleteNode.show();
|
||||
}
|
||||
|
||||
onRemoveResponse() {
|
||||
const childId = this.removedChild.id;
|
||||
const path = `departments/${childId}/removeChild`;
|
||||
this.$http.post(path).then(() => {
|
||||
this.$.treeview.remove(this.removedChild);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnWorkerDepartment', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
});
|
||||
import './main';
|
||||
import './index/';
|
||||
import './summary';
|
||||
import './card';
|
||||
import './descriptor';
|
||||
import './basic-data';
|
||||
import './descriptor-popover';
|
||||
|
|
|
@ -3,6 +3,14 @@
|
|||
url="departments/getLeaves"
|
||||
auto-load="false">
|
||||
</vn-crud-model>
|
||||
<vn-portal slot="topbar">
|
||||
<vn-searchbar
|
||||
vn-focus
|
||||
info="Search departments by id, name or code"
|
||||
on-search="$ctrl.onSearch($params)"
|
||||
base-state="worker.department">
|
||||
</vn-searchbar>
|
||||
</vn-portal>
|
||||
<form name="form" class="vn-w-md">
|
||||
<vn-card class="vn-pa-md">
|
||||
<vn-treeview vn-id="treeview" root-label="Departments" read-only="false"
|
||||
|
@ -13,7 +21,12 @@
|
|||
on-drop="$ctrl.onDrop($dropped, $dragged)"
|
||||
on-drag-start="$ctrl.onDragStart(item)"
|
||||
on-drag-end="$ctrl.onDragEnd(item)">
|
||||
{{::item.name}}
|
||||
<a
|
||||
style="display: block; color: inherit;"
|
||||
ui-sref="worker.department.card.summary({id: item.id})"
|
||||
translate>
|
||||
{{::item.name}}
|
||||
</a>
|
||||
</vn-treeview>
|
||||
</vn-card>
|
||||
</form>
|
|
@ -0,0 +1,94 @@
|
|||
import ngModule from '../../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
class Controller extends Section {
|
||||
$postLink() {
|
||||
this.$.treeview.fetch();
|
||||
if (this.$params.q) {
|
||||
const search = JSON.parse(this.$params.q);
|
||||
this.onSearch(search);
|
||||
}
|
||||
}
|
||||
|
||||
onSearch(params) {
|
||||
this.$.model.applyFilter({}, params).then(() => {
|
||||
const data = this.$.model.data;
|
||||
this.$.treeview.data = data;
|
||||
});
|
||||
}
|
||||
|
||||
onFetch(item) {
|
||||
const params = item ? {parentId: item.id} : null;
|
||||
return this.$.model.applyFilter({}, params).then(() => {
|
||||
return this.$.model.data;
|
||||
});
|
||||
}
|
||||
|
||||
onSort(a, b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
onDrop(dropped, dragged) {
|
||||
const params = dropped ? {parentId: dropped.id} : null;
|
||||
const query = `departments/${dragged.id}/moveChild`;
|
||||
this.$http.post(query, params).then(() => {
|
||||
this.$.treeview.move(dragged, dropped);
|
||||
});
|
||||
}
|
||||
|
||||
onCreate(parent) {
|
||||
this.newChild = {
|
||||
parent: parent,
|
||||
name: ''
|
||||
};
|
||||
|
||||
this.$.createNode.show();
|
||||
}
|
||||
|
||||
onCreateDialogOpen() {
|
||||
this.newChild.name = '';
|
||||
}
|
||||
|
||||
onCreateResponse() {
|
||||
try {
|
||||
if (!this.newChild.name)
|
||||
throw new Error(`Name can't be empty`);
|
||||
|
||||
const params = {name: this.newChild.name};
|
||||
const parent = this.newChild.parent;
|
||||
|
||||
if (parent && parent.id)
|
||||
params.parentId = parent.id;
|
||||
|
||||
const query = `departments/createChild`;
|
||||
this.$http.post(query, params).then(res => {
|
||||
const item = res.data;
|
||||
item.parent = parent;
|
||||
|
||||
this.$.treeview.create(item);
|
||||
});
|
||||
} catch (e) {
|
||||
this.vnApp.showError(this.$t(e.message));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onRemove(item) {
|
||||
this.removedChild = item;
|
||||
this.$.deleteNode.show();
|
||||
}
|
||||
|
||||
onRemoveResponse() {
|
||||
const childId = this.removedChild.id;
|
||||
const path = `departments/${childId}/removeChild`;
|
||||
this.$http.post(path).then(() => {
|
||||
this.$.treeview.remove(this.removedChild);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnWorkerDepartmentIndex', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
New department: Nuevo departamento
|
||||
Delete department: Eliminar departamento
|
||||
Are you sure you want to delete it?: ¿Seguro que quieres eliminarlo?
|
||||
Name can't be empty: El nombre esta vacio
|
||||
Name can't be empty: El nombre esta vacio
|
||||
Department workers: Trabajadores del departamento
|
|
@ -0,0 +1,9 @@
|
|||
<vn-crud-model
|
||||
vn-id="model"
|
||||
url="Departments/filter"
|
||||
limit="20"
|
||||
order="id">
|
||||
</vn-crud-model>
|
||||
<ui-view>
|
||||
<vn-worker-department-index></vn-worker-department-index>
|
||||
</ui-view>
|
|
@ -0,0 +1,7 @@
|
|||
import ngModule from '../../module';
|
||||
import ModuleMain from 'salix/components/module-main';
|
||||
|
||||
ngModule.vnComponent('vnWorkerDepartment', {
|
||||
controller: ModuleMain,
|
||||
template: require('./index.html')
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
<vn-portal slot="topbar">
|
||||
<vn-searchbar
|
||||
vn-focus
|
||||
info="Search departments by id, name or code"
|
||||
on-search="$ctrl.onSearch($params)"
|
||||
base-state="worker.department">
|
||||
</vn-searchbar>
|
||||
</vn-portal>
|
||||
<vn-card class="summary">
|
||||
<h5>
|
||||
<span>{{summary.name}}</span>
|
||||
</h5>
|
||||
<vn-horizontal class="vn-pa-md">
|
||||
<vn-one class="expand">
|
||||
<h4 ng-show="$ctrl.isHr">
|
||||
<a
|
||||
ui-sref="worker.department.card.basicData({id:summary.id})">
|
||||
<span translate vn-tooltip="Go to">Basic data</span>
|
||||
</a>
|
||||
</h4>
|
||||
<h4
|
||||
translates
|
||||
ng-show="!$ctrl.isHr">
|
||||
Basic data
|
||||
</h4>
|
||||
</vn-one>
|
||||
<vn-one>
|
||||
<vn-vertical>
|
||||
<vn-label-value label="Nombre"
|
||||
value="{{summary.name}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Code"
|
||||
value="{{summary.code}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Chat"
|
||||
value="{{summary.chatName}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value
|
||||
label="Boss department">
|
||||
<span
|
||||
ng-click="workerDescriptor.show($event, summary.worker.id)"
|
||||
class="link">
|
||||
{{summary.worker.user.name}}
|
||||
</span>
|
||||
</vn-label-value>
|
||||
<vn-label-value
|
||||
label="Email"
|
||||
value="{{summary.notificationEmail}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Self-consumption customer"
|
||||
value="{{summary.client.name}}">
|
||||
</vn-label-value>
|
||||
</vn-vertical>
|
||||
</vn-one>
|
||||
<vn-one>
|
||||
<vn-vertical>
|
||||
<vn-check
|
||||
label="Telework"
|
||||
ng-model="summary.isTeleworking"
|
||||
disabled="true">
|
||||
</vn-check>
|
||||
<vn-check
|
||||
label="Notify on errors"
|
||||
ng-model="summary.hasToMistake"
|
||||
disabled="true">
|
||||
</vn-check>
|
||||
<vn-check
|
||||
label="worksInProduction"
|
||||
ng-model="summary.isProduction"
|
||||
disabled="true">
|
||||
</vn-check>
|
||||
<vn-check
|
||||
label="Fill in days without physical check-ins"
|
||||
ng-model="summary.hasToRefill"
|
||||
disabled="true">
|
||||
</vn-check>
|
||||
<vn-check
|
||||
label="Send check-ins by email"
|
||||
ng-model="summary.hasToSendMail"
|
||||
disabled="true">
|
||||
</vn-check>
|
||||
</vn-vertical>
|
||||
</vn-one>
|
||||
</vn-horizontal>
|
||||
</vn-card>
|
||||
<vn-worker-descriptor-popover
|
||||
vn-id="workerDescriptor">
|
||||
</vn-worker-descriptor-popover>
|
||||
<vn-worker-department-descriptor-popover
|
||||
vn-id="workerDepartmentDescriptor">
|
||||
</vn-worker-department-descriptor-popover>
|
|
@ -0,0 +1,27 @@
|
|||
import ngModule from '../../module';
|
||||
import Component from 'core/lib/component';
|
||||
import './style.scss';
|
||||
|
||||
class Controller extends Component {
|
||||
set department(value) {
|
||||
this._department = value;
|
||||
this.$.summary = null;
|
||||
if (!value) return;
|
||||
this.$.summary = this.department;
|
||||
}
|
||||
get department() {
|
||||
return this._department;
|
||||
}
|
||||
|
||||
get isHr() {
|
||||
return this.aclService.hasAny(['hr']);
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.component('vnWorkerDepartmentSummary', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller,
|
||||
bindings: {
|
||||
department: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
Name: Nombre
|
||||
Code: Código
|
||||
Chat: Chat
|
||||
Boss department: Jefe de departamento
|
||||
Email: Email
|
||||
Self-consumption customer: Cliente autoconsumo
|
||||
Telework: Teletrabaja
|
||||
Notify on errors: Notificar errores
|
||||
worksInProduction: Pertenece a producción
|
||||
Fill in days without physical check-ins: Completar días sin registros físicos
|
||||
Send check-ins by email: Enviar fichadas por mail
|
||||
Are you sure you want to delete this department?: ¿Estás seguro de que quieres eliminar este departamento?
|
|
@ -0,0 +1,8 @@
|
|||
@import "./variables";
|
||||
|
||||
vn-worker-department-summary {
|
||||
.expand {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,9 @@
|
|||
"description": "Wikipedia"
|
||||
},
|
||||
{"state": "worker.card.workerLog", "icon": "history"}
|
||||
],
|
||||
"department": [
|
||||
{"state": "worker.department.card.basicData", "icon": "settings"}
|
||||
]
|
||||
},
|
||||
"keybindings": [
|
||||
|
@ -118,12 +121,35 @@
|
|||
"worker": "$ctrl.worker"
|
||||
}
|
||||
}, {
|
||||
"url" : "/department",
|
||||
"url": "/department?q",
|
||||
"state": "worker.department",
|
||||
"component": "vn-worker-department",
|
||||
"description": "Departments",
|
||||
"acl": ["hr"]
|
||||
"description":"Departments"
|
||||
}, {
|
||||
"url": "/:id",
|
||||
"state": "worker.department.card",
|
||||
"component": "vn-worker-department-card",
|
||||
"abstract": true,
|
||||
"description": "Detail"
|
||||
}, {
|
||||
"url" : "/summary",
|
||||
"state": "worker.department.card.summary",
|
||||
"component": "vn-worker-department-summary",
|
||||
"description": "Summary",
|
||||
"params": {
|
||||
"department": "$ctrl.department"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "/basic-data",
|
||||
"state": "worker.department.card.basicData",
|
||||
"component": "vn-worker-department-basic-data",
|
||||
"description": "Basic data",
|
||||
"params": {
|
||||
"department": "$ctrl.department"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "/dms",
|
||||
"state": "worker.card.dms",
|
||||
"abstract": true,
|
||||
|
|
|
@ -27,15 +27,19 @@
|
|||
<vn-label-value label="Email" no-ellipsize
|
||||
value="{{worker.user.emailUser.email}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Department"
|
||||
value="{{worker.department.department.name}}">
|
||||
<vn-label-value label="Department">
|
||||
<span
|
||||
ng-click="workerDepartmentDescriptor.show($event, worker.department.department.id)"
|
||||
class="link">
|
||||
{{worker.department.department.name}}
|
||||
</span>
|
||||
</vn-label-value>
|
||||
<vn-label-value
|
||||
label="Boss">
|
||||
<span
|
||||
ng-click="workerDescriptor.show($event, worker.boss.id)"
|
||||
class="link">
|
||||
{{::worker.boss.nickname}}
|
||||
{{::worker.boss.name}}
|
||||
</span>
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Mobile extension"
|
||||
|
@ -71,3 +75,6 @@
|
|||
<vn-worker-descriptor-popover
|
||||
vn-id="workerDescriptor">
|
||||
</vn-worker-descriptor-popover>
|
||||
<vn-worker-department-descriptor-popover
|
||||
vn-id="workerDepartmentDescriptor">
|
||||
</vn-worker-department-descriptor-popover>
|
||||
|
|
|
@ -38,7 +38,7 @@ class Controller extends Summary {
|
|||
},
|
||||
{
|
||||
relation: 'boss',
|
||||
scope: {fields: ['id', 'nickname']}
|
||||
scope: {fields: ['id', 'name']}
|
||||
},
|
||||
{
|
||||
relation: 'sip',
|
||||
|
@ -48,7 +48,8 @@ class Controller extends Summary {
|
|||
relation: 'department',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'department'
|
||||
relation: 'department',
|
||||
scope: {fields: ['id', 'code', 'name']}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,106 +4,114 @@
|
|||
filter="::$ctrl.filter"
|
||||
data="$ctrl.hours">
|
||||
</vn-crud-model>
|
||||
<vn-card class="vn-pa-lg vn-w-lg">
|
||||
<vn-table model="model" auto-load="false">
|
||||
<vn-thead>
|
||||
<vn-tr>
|
||||
<vn-td ng-repeat="weekday in $ctrl.weekDays" center>
|
||||
<div class="weekday" translate>{{::$ctrl.weekdayNames[$index].name}}</div>
|
||||
<div>
|
||||
<span>{{::weekday.dated | date: 'dd'}}</span>
|
||||
<span title="{{::weekday.dated | date: 'MMMM' | translate}}" translate>
|
||||
{{::weekday.dated | date: 'MMMM'}}
|
||||
</span>
|
||||
</div>
|
||||
<vn-chip
|
||||
title="{{::weekday.event.name}}"
|
||||
ng-class="{invisible: !weekday.event}">
|
||||
<vn-avatar
|
||||
ng-style="::{backgroundColor: weekday.event.color}">
|
||||
</vn-avatar>
|
||||
<div ng-if="$ctrl.worker.hasWorkCenter">
|
||||
<vn-card class="vn-pa-lg vn-w-lg">
|
||||
<vn-table model="model" auto-load="false">
|
||||
<vn-thead>
|
||||
<vn-tr>
|
||||
<vn-td ng-repeat="weekday in $ctrl.weekDays" center>
|
||||
<div class="weekday" translate>{{::$ctrl.weekdayNames[$index].name}}</div>
|
||||
<div>
|
||||
{{::weekday.event.name}}
|
||||
<span>{{::weekday.dated | date: 'dd'}}</span>
|
||||
<span title="{{::weekday.dated | date: 'MMMM' | translate}}" translate>
|
||||
{{::weekday.dated | date: 'MMMM'}}
|
||||
</span>
|
||||
</div>
|
||||
</vn-chip>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-thead>
|
||||
<vn-tbody>
|
||||
<vn-tr>
|
||||
<vn-td ng-repeat="weekday in $ctrl.weekDays" class="hours vn-pa-none" expand>
|
||||
<section ng-repeat="hour in weekday.hours">
|
||||
<vn-icon
|
||||
icon="{{
|
||||
::hour.direction == 'in' ? 'arrow_forward' : 'arrow_back'
|
||||
}}"
|
||||
title="{{
|
||||
::(hour.direction == 'in' ? 'In' : 'Out') | translate
|
||||
}}"
|
||||
ng-class="::{'invisible': hour.direction == 'middle'}">
|
||||
</vn-icon>
|
||||
<vn-chip
|
||||
ng-class="::{'colored': hour.manual, 'clickable': true}"
|
||||
removable="::hour.manual"
|
||||
on-remove="$ctrl.showDeleteDialog($event, hour)"
|
||||
ng-click="$ctrl.edit($event, hour)"
|
||||
>
|
||||
<prepend>
|
||||
<vn-icon icon="edit"
|
||||
vn-tooltip="Edit">
|
||||
</vn-icon>
|
||||
</prepend>
|
||||
{{::hour.timed | date: 'HH:mm'}}
|
||||
title="{{::weekday.event.name}}"
|
||||
ng-class="{invisible: !weekday.event}">
|
||||
<vn-avatar
|
||||
ng-style="::{backgroundColor: weekday.event.color}">
|
||||
</vn-avatar>
|
||||
<div>
|
||||
{{::weekday.event.name}}
|
||||
</div>
|
||||
</vn-chip>
|
||||
</section>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-tbody>
|
||||
<vn-tfoot>
|
||||
<vn-tr>
|
||||
<vn-td ng-repeat="weekday in $ctrl.weekDays" center>
|
||||
{{$ctrl.formatHours(weekday.workedHours)}} h.
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
<vn-tr>
|
||||
<vn-td center ng-repeat="weekday in $ctrl.weekDays">
|
||||
<vn-icon-button
|
||||
icon="add_circle"
|
||||
vn-tooltip="Add time"
|
||||
ng-click="$ctrl.showAddTimeDialog(weekday)">
|
||||
</vn-icon-button>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-tfoot>
|
||||
</vn-table>
|
||||
</vn-card>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-thead>
|
||||
<vn-tbody>
|
||||
<vn-tr>
|
||||
<vn-td ng-repeat="weekday in $ctrl.weekDays" class="hours vn-pa-none" expand>
|
||||
<section ng-repeat="hour in weekday.hours">
|
||||
<vn-icon
|
||||
icon="{{
|
||||
::hour.direction == 'in' ? 'arrow_forward' : 'arrow_back'
|
||||
}}"
|
||||
title="{{
|
||||
::(hour.direction == 'in' ? 'In' : 'Out') | translate
|
||||
}}"
|
||||
ng-class="::{'invisible': hour.direction == 'middle'}">
|
||||
</vn-icon>
|
||||
<vn-chip
|
||||
ng-class="::{'colored': hour.manual, 'clickable': true}"
|
||||
removable="::hour.manual"
|
||||
on-remove="$ctrl.showDeleteDialog($event, hour)"
|
||||
ng-click="$ctrl.edit($event, hour)"
|
||||
>
|
||||
<prepend>
|
||||
<vn-icon icon="edit"
|
||||
vn-tooltip="Edit">
|
||||
</vn-icon>
|
||||
</prepend>
|
||||
{{::hour.timed | date: 'HH:mm'}}
|
||||
</vn-chip>
|
||||
</section>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-tbody>
|
||||
<vn-tfoot>
|
||||
<vn-tr>
|
||||
<vn-td ng-repeat="weekday in $ctrl.weekDays" center>
|
||||
{{$ctrl.formatHours(weekday.workedHours)}} h.
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
<vn-tr>
|
||||
<vn-td center ng-repeat="weekday in $ctrl.weekDays">
|
||||
<vn-icon-button
|
||||
icon="add_circle"
|
||||
vn-tooltip="Add time"
|
||||
ng-click="$ctrl.showAddTimeDialog(weekday)">
|
||||
</vn-icon-button>
|
||||
</vn-td>
|
||||
</vn-tr>
|
||||
</vn-tfoot>
|
||||
</vn-table>
|
||||
</vn-card>
|
||||
|
||||
<vn-button-bar ng-show="$ctrl.state" class="vn-w-lg">
|
||||
<vn-button
|
||||
label="Satisfied"
|
||||
disabled="$ctrl.state == 'CONFIRMED'"
|
||||
ng-if="$ctrl.isHimSelf"
|
||||
ng-click="$ctrl.isSatisfied()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Not satisfied"
|
||||
disabled="$ctrl.state == 'REVISE'"
|
||||
ng-if="$ctrl.isHimSelf"
|
||||
ng-click="reason.show()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Reason"
|
||||
ng-if="$ctrl.reason && ($ctrl.isHimSelf || $ctrl.isHr)"
|
||||
ng-click="reason.show()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Resend"
|
||||
ng-click="sendEmailConfirmation.show()"
|
||||
class="right"
|
||||
vn-tooltip="Resend email of this week to the user"
|
||||
ng-show="::$ctrl.isHr">
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
<vn-button-bar ng-show="$ctrl.state" class="vn-w-lg">
|
||||
<vn-button
|
||||
label="Satisfied"
|
||||
disabled="$ctrl.state == 'CONFIRMED'"
|
||||
ng-if="$ctrl.isHimSelf"
|
||||
ng-click="$ctrl.isSatisfied()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Not satisfied"
|
||||
disabled="$ctrl.state == 'REVISE'"
|
||||
ng-if="$ctrl.isHimSelf"
|
||||
ng-click="reason.show()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Reason"
|
||||
ng-if="$ctrl.reason && ($ctrl.isHimSelf || $ctrl.isHr)"
|
||||
ng-click="reason.show()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
label="Resend"
|
||||
ng-click="sendEmailConfirmation.show()"
|
||||
class="right"
|
||||
vn-tooltip="Resend email of this week to the user"
|
||||
ng-show="::$ctrl.isHr">
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
</div>
|
||||
<div
|
||||
ng-if="!$ctrl.worker.hasWorkCenter"
|
||||
class="bg-title"
|
||||
translate>
|
||||
Autonomous worker
|
||||
</div>
|
||||
|
||||
<vn-side-menu side="right">
|
||||
<div class="vn-pa-md">
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue