Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 3095-item.shelving
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Vicent Llopis 2022-10-11 07:09:20 +02:00
commit 7b7e01f49e
64 changed files with 48596 additions and 344 deletions

View File

@ -0,0 +1,58 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('privileges', {
description: 'Change role and hasGrant if user has privileges',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The user id',
http: {source: 'path'}
},
{
arg: 'roleFk',
type: 'number',
description: 'The new role for user',
},
{
arg: 'hasGrant',
type: 'boolean',
description: 'Whether to has grant'
}
],
http: {
path: `/:id/privileges`,
verb: 'POST'
}
});
Self.privileges = async function(ctx, id, roleFk, hasGrant, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const user = await models.Account.findById(userId, null, myOptions);
if (!user.hasGrant)
throw new UserError(`You don't have enough privileges`);
const userToUpdate = await models.Account.findById(id);
if (hasGrant != null)
return await userToUpdate.updateAttribute('hasGrant', hasGrant, myOptions);
if (!roleFk) return;
const role = await models.Role.findById(roleFk, null, myOptions);
const hasRole = await models.Account.hasRole(userId, role.name, myOptions);
if (!hasRole)
throw new UserError(`You don't have enough privileges`);
await userToUpdate.updateAttribute('roleFk', roleFk, myOptions);
};
};

View File

@ -0,0 +1,99 @@
const models = require('vn-loopback/server/server').models;
describe('account privileges()', () => {
const employeeId = 1;
const developerId = 9;
const sysadminId = 66;
const bruceWayneId = 1101;
it('should throw an error when user not has privileges', async() => {
const ctx = {req: {accessToken: {userId: developerId}}};
const tx = await models.Account.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Account.privileges(ctx, employeeId, null, true, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toContain(`You don't have enough privileges`);
});
it('should throw an error when user has privileges but not has the role', async() => {
const ctx = {req: {accessToken: {userId: sysadminId}}};
const tx = await models.Account.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const root = await models.Role.findOne({
where: {
name: 'root'
}
}, options);
await models.Account.privileges(ctx, employeeId, root.id, null, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toContain(`You don't have enough privileges`);
});
it('should change role', async() => {
const ctx = {req: {accessToken: {userId: sysadminId}}};
const tx = await models.Account.beginTransaction({});
const options = {transaction: tx};
const agency = await models.Role.findOne({
where: {
name: 'agency'
}
}, options);
let error;
let result;
try {
await models.Account.privileges(ctx, bruceWayneId, agency.id, null, options);
result = await models.Account.findById(bruceWayneId, null, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error).not.toBeDefined();
expect(result.roleFk).toEqual(agency.id);
});
it('should change hasGrant', async() => {
const ctx = {req: {accessToken: {userId: sysadminId}}};
const tx = await models.Account.beginTransaction({});
let error;
let result;
try {
const options = {transaction: tx};
await models.Account.privileges(ctx, bruceWayneId, null, true, options);
result = await models.Account.findById(bruceWayneId, null, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error).not.toBeDefined();
expect(result.hasGrant).toBeTruthy();
});
});

View File

@ -7,6 +7,7 @@ module.exports = Self => {
require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self);
require('../methods/account/validate-token')(Self);
require('../methods/account/privileges')(Self);
// Validations
@ -77,7 +78,7 @@ module.exports = Self => {
`SELECT r.name
FROM account.user u
JOIN account.roleRole rr ON rr.role = u.role
JOIN account.role r ON r.id = rr.inheritsFrom
JOIN account.role r ON r.id = rr.inheritsFrom
WHERE u.id = ?`, [userId], options);
let roles = [];

View File

@ -48,6 +48,9 @@
},
"image": {
"type": "string"
},
"hasGrant": {
"type": "boolean"
}
},
"relations": {

View File

@ -7,13 +7,18 @@ process.on('warning', warning => {
console.log(warning.stack);
});
process.on('exit', async function() {
if (container) await container.rm();
});
let container;
async function test() {
let isCI = false;
if (process.argv[2] === 'ci')
isCI = true;
const container = new Docker();
container = new Docker();
await container.run(isCI);
dataSources = JSON.parse(JSON.stringify(dataSources));
@ -41,8 +46,6 @@ async function test() {
}
}));
jasmine.exitOnCompletion = false;
if (isCI) {
const JunitReporter = require('jasmine-reporters');
jasmine.addReporter(new JunitReporter.JUnitXmlReporter());

View File

@ -0,0 +1 @@
ALTER TABLE `account`.`user` ADD hasGrant TINYINT(1) NOT NULL;

View File

View File

@ -19,8 +19,9 @@ module.exports = class Docker {
* to avoid a bug with OverlayFS driver on MacOS.
*
* @param {Boolean} ci continuous integration environment argument
* @param {String} networkName Name of the container network
*/
async run(ci) {
async run(ci, networkName = 'jenkins') {
let d = new Date();
let pad = v => v < 10 ? '0' + v : v;
let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
@ -43,7 +44,7 @@ module.exports = class Docker {
let runChown = process.platform != 'linux';
let network = '';
if (ci) network = '--network="jenkins"';
if (ci) network = `--network="${networkName}"`;
log('Starting container...');
const container = await this.execP(`
@ -60,7 +61,7 @@ module.exports = class Docker {
let netSettings = JSON.parse(inspect.stdout);
if (ci) {
this.dbConf.host = netSettings.Networks.jenkins.IPAddress;
this.dbConf.host = netSettings.Networks[networkName].IPAddress;
this.dbConf.port = 3306;
} else
this.dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort'];

View File

@ -2663,3 +2663,7 @@ INSERT INTO `vn`.`collection` (`id`, `created`, `workerFk`, `stateFk`, `itemPack
INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `level`, `wagon`, `smartTagFk`, `usedShelves`, `itemCount`, `liters`)
VALUES
(9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL);
UPDATE `account`.`user`
SET `hasGrant` = 1
WHERE `id` = 66;

View File

@ -51,14 +51,12 @@ export default {
accountDescriptor: {
menuButton: 'vn-user-descriptor vn-icon-button[icon="more_vert"]',
deleteAccount: '.vn-menu [name="deleteUser"]',
changeRole: '.vn-menu [name="changeRole"]',
setPassword: '.vn-menu [name="setPassword"]',
activateAccount: '.vn-menu [name="enableAccount"]',
activateUser: '.vn-menu [name="activateUser"]',
deactivateUser: '.vn-menu [name="deactivateUser"]',
newPassword: 'vn-textfield[ng-model="$ctrl.newPassword"]',
repeatPassword: 'vn-textfield[ng-model="$ctrl.repeatPassword"]',
newRole: 'vn-autocomplete[ng-model="$ctrl.newRole"]',
activeAccountIcon: 'vn-icon[icon="contact_mail"]',
activeUserIcon: 'vn-icon[icon="icon-disabled"]',
acceptButton: 'button[response="accept"]',
@ -143,6 +141,11 @@ export default {
verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]',
save: 'vn-account-samba vn-submit'
},
accountPrivileges: {
checkHasGrant: 'vn-user-privileges vn-check[ng-model="$ctrl.user.hasGrant"]',
role: 'vn-user-privileges vn-autocomplete[ng-model="$ctrl.user.roleFk"]',
save: 'vn-user-privileges vn-submit'
},
clientsIndex: {
createClientButton: `vn-float-button`
},
@ -835,14 +838,16 @@ export default {
confirmButton: '.vn-confirm.shown button[response="accept"]',
},
routeIndex: {
anyResult: 'vn-table a',
firstRouteCheckbox: 'a:nth-child(1) vn-td:nth-child(1) > vn-check',
anyResult: 'vn-route-index tbody > tr',
firstRouteCheckbox: 'vn-route-index tbody > tr:nth-child(1) > td:nth-child(1) > vn-check',
addNewRouteButton: 'vn-route-index a[ui-sref="route.create"]',
cloneButton: 'vn-route-index button > vn-icon[icon="icon-clone"]',
submitClonationButton: 'tpl-buttons > button[response="accept"]',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
searchAgencyAutocomlete: 'vn-route-search-panel vn-autocomplete[ng-model="filter.agencyModeFk"]',
advancedSearchButton: 'vn-route-search-panel button[type=submit]',
previewButton: 'vn-route-index tbody > tr:nth-child(7) > td:nth-child(11) > vn-icon-button[icon="preview"]',
},
createRouteView: {
worker: 'vn-route-create vn-autocomplete[ng-model="$ctrl.route.workerFk"]',
@ -862,6 +867,8 @@ export default {
firstTicketDescriptor: '.vn-popover.shown vn-ticket-descriptor',
firstAlias: 'vn-route-summary vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(3) > span',
firstClientDescriptor: '.vn-popover.shown vn-client-descriptor',
goToRouteSummaryButton: 'vn-route-summary > vn-card > h5 > a',
},
routeBasicData: {
worker: 'vn-route-basic-data vn-autocomplete[ng-model="$ctrl.route.workerFk"]',

View File

@ -8,7 +8,7 @@ describe('Client Edit web access path', () => {
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'client');
await page.loginAndModule('salesPerson', 'client');
await page.accessToSearchResult('max');
await page.accessToSection('client.card.webAccess');
});

View File

@ -9,7 +9,8 @@ describe('Route summary path', () => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'route');
await page.waitToClick('vn-route-index vn-tbody > a:nth-child(7)');
await page.waitToClick(selectors.routeIndex.previewButton);
await page.waitToClick(selectors.routeSummary.goToRouteSummaryButton);
});
afterAll(async() => {
@ -34,6 +35,8 @@ describe('Route summary path', () => {
});
it('should click on the first ticket ID making the descriptor popover visible', async() => {
await page.waitForState('route.card.summary');
await page.waitForTimeout(250);
await page.waitToClick(selectors.routeSummary.firstTicketID);
await page.waitForSelector(selectors.routeSummary.firstTicketDescriptor);
const visible = await page.isVisible(selectors.routeSummary.firstTicketDescriptor);

View File

@ -62,27 +62,6 @@ describe('Account create and basic data path', () => {
});
describe('Descriptor option', () => {
describe('Edit role', () => {
it('should edit the role using the descriptor menu', async() => {
await page.waitToClick(selectors.accountDescriptor.menuButton);
await page.waitToClick(selectors.accountDescriptor.changeRole);
await page.autocompleteSearch(selectors.accountDescriptor.newRole, 'adminBoss');
await page.waitToClick(selectors.accountDescriptor.acceptButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Role changed succesfully!');
});
it('should reload the roles section to see now there are more roles', async() => {
// when role updates db takes time to return changes, without this timeout the result would have been 3
await page.waitForTimeout(1000);
await page.reloadSection('account.card.roles');
const rolesCount = await page.countElement(selectors.accountRoles.anyResult);
expect(rolesCount).toEqual(61);
});
});
describe('activate account', () => {
it(`should check the active account icon isn't present in the descriptor`, async() => {
await page.waitForNumberOfElements(selectors.accountDescriptor.activeAccountIcon, 0);

View File

@ -0,0 +1,86 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Account privileges path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('developer', 'account');
await page.accessToSearchResult('1101');
await page.accessToSection('account.card.privileges');
});
afterAll(async() => {
await browser.close();
});
describe('as developer', () => {
it('should throw error when give privileges', async() => {
await page.waitToClick(selectors.accountPrivileges.checkHasGrant);
await page.waitToClick(selectors.accountPrivileges.save);
const message = await page.waitForSnackbar();
expect(message.text).toContain(`You don't have enough privileges`);
});
it('should throw error when change role', async() => {
await page.autocompleteSearch(selectors.accountPrivileges.role, 'employee');
await page.waitToClick(selectors.accountPrivileges.save);
const message = await page.waitForSnackbar();
expect(message.text).toContain(`You don't have enough privileges`);
});
});
describe('as sysadmin', () => {
beforeAll(async() => {
await page.loginAndModule('sysadmin', 'account');
await page.accessToSearchResult('9');
await page.accessToSection('account.card.privileges');
});
it('should give privileges', async() => {
await page.waitToClick(selectors.accountPrivileges.checkHasGrant);
await page.waitToClick(selectors.accountPrivileges.save);
const message = await page.waitForSnackbar();
await page.reloadSection('account.card.privileges');
const result = await page.checkboxState(selectors.accountPrivileges.checkHasGrant);
expect(message.text).toContain(`Data saved!`);
expect(result).toBe('checked');
});
it('should change role', async() => {
await page.autocompleteSearch(selectors.accountPrivileges.role, 'employee');
await page.waitToClick(selectors.accountPrivileges.save);
const message = await page.waitForSnackbar();
await page.reloadSection('account.card.privileges');
const result = await page.waitToGetProperty(selectors.accountPrivileges.role, 'value');
expect(message.text).toContain(`Data saved!`);
expect(result).toContain('employee');
});
});
describe('as developer again', () => {
it('should remove privileges', async() => {
await page.accessToSearchResult('9');
await page.accessToSection('account.card.privileges');
await page.waitToClick(selectors.accountPrivileges.checkHasGrant);
await page.waitToClick(selectors.accountPrivileges.save);
await page.reloadSection('account.card.privileges');
const result = await page.checkboxState(selectors.accountPrivileges.checkHasGrant);
expect(result).toBe('unchecked');
});
});
});

221
front/package-lock.json generated
View File

@ -1,78 +1,215 @@
{
"name": "salix-front",
"version": "1.0.0",
"lockfileVersion": 1,
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "salix-front",
"version": "1.0.0",
"license": "GPL-3.0",
"dependencies": {
"@uirouter/angularjs": "^1.0.20",
"angular": "^1.7.5",
"angular-animate": "^1.7.8",
"angular-moment": "^1.3.0",
"angular-translate": "^2.18.1",
"angular-translate-loader-partial": "^2.18.1",
"croppie": "^2.6.5",
"js-yaml": "^3.13.1",
"mg-crud": "^1.1.2",
"oclazyload": "^0.6.3",
"require-yaml": "0.0.1",
"validator": "^6.3.0"
}
},
"node_modules/@uirouter/angularjs": {
"version": "1.0.29",
"license": "MIT",
"dependencies": {
"@uirouter/core": "6.0.7"
},
"engines": {
"node": ">=4.0.0"
},
"peerDependencies": {
"angular": ">=1.2.0"
}
},
"node_modules/@uirouter/core": {
"version": "6.0.7",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/angular": {
"version": "1.8.2",
"license": "MIT"
},
"node_modules/angular-animate": {
"version": "1.8.2",
"license": "MIT"
},
"node_modules/angular-moment": {
"version": "1.3.0",
"license": "MIT",
"dependencies": {
"moment": ">=2.8.0 <3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/angular-translate": {
"version": "2.18.4",
"license": "MIT",
"dependencies": {
"angular": "^1.8.0"
},
"engines": {
"node": "*"
}
},
"node_modules/angular-translate-loader-partial": {
"version": "2.18.4",
"license": "MIT",
"dependencies": {
"angular-translate": "~2.18.4"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/croppie": {
"version": "2.6.5",
"license": "MIT"
},
"node_modules/esprima": {
"version": "4.0.1",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/js-yaml": {
"version": "3.14.1",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/mg-crud": {
"version": "1.1.2",
"license": "MIT",
"dependencies": {
"angular": "^1.6.1"
}
},
"node_modules/moment": {
"version": "2.29.1",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/oclazyload": {
"version": "0.6.3",
"license": "MIT"
},
"node_modules/require-yaml": {
"version": "0.0.1",
"license": "BSD",
"dependencies": {
"js-yaml": ""
}
},
"node_modules/require-yaml/node_modules/argparse": {
"version": "2.0.1",
"license": "Python-2.0"
},
"node_modules/require-yaml/node_modules/js-yaml": {
"version": "4.1.0",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"license": "BSD-3-Clause"
},
"node_modules/validator": {
"version": "6.3.0",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
}
},
"dependencies": {
"@uirouter/angularjs": {
"version": "1.0.29",
"resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.29.tgz",
"integrity": "sha512-RImWnBarNixkMto0o8stEaGwZmvhv5cnuOLXyMU2pY8MP2rgEF74ZNJTLeJCW14LR7XDUxVH8Mk8bPI6lxedmQ==",
"requires": {
"@uirouter/core": "6.0.7"
}
},
"@uirouter/core": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.7.tgz",
"integrity": "sha512-KUTJxL+6q0PiBnFx4/Z+Hsyg0pSGiaW5yZQeJmUxknecjpTbnXkLU8H2EqRn9N2B+qDRa7Jg8RcgeNDPY72O1w=="
"version": "6.0.7"
},
"angular": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.2.tgz",
"integrity": "sha512-IauMOej2xEe7/7Ennahkbb5qd/HFADiNuLSESz9Q27inmi32zB0lnAsFeLEWcox3Gd1F6YhNd1CP7/9IukJ0Gw=="
"version": "1.8.2"
},
"angular-animate": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz",
"integrity": "sha512-Jbr9+grNMs9Kj57xuBU3Ju3NOPAjS1+g2UAwwDv7su1lt0/PLDy+9zEwDiu8C8xJceoTbmBNKiWGPJGBdCQLlA=="
"version": "1.8.2"
},
"angular-moment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz",
"integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==",
"requires": {
"moment": ">=2.8.0 <3.0.0"
}
},
"angular-translate": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.4.tgz",
"integrity": "sha512-KohNrkH6J9PK+VW0L/nsRTcg5Fw70Ajwwe3Jbfm54Pf9u9Fd+wuingoKv+h45mKf38eT+Ouu51FPua8VmZNoCw==",
"requires": {
"angular": "^1.8.0"
}
},
"angular-translate-loader-partial": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate-loader-partial/-/angular-translate-loader-partial-2.18.4.tgz",
"integrity": "sha512-bsjR+FbB0sdA2528E/ugwKdlPPQhA1looxLxI3otayBTFXBpED33besfSZhYAISLgNMSL038vSssfRUen9qD8w==",
"requires": {
"angular-translate": "~2.18.4"
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"croppie": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.5.tgz",
"integrity": "sha512-IlChnVUGG5T3w2gRZIaQgBtlvyuYnlUWs2YZIXXR3H9KrlO1PtBT3j+ykxvy9eZIWhk+V5SpBmhCQz5UXKrEKQ=="
"version": "2.6.5"
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
"version": "4.0.1"
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@ -80,39 +217,27 @@
},
"mg-crud": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/mg-crud/-/mg-crud-1.1.2.tgz",
"integrity": "sha1-p6AWGzWSPK7/8ZpIBpS2V1vDggw=",
"requires": {
"angular": "^1.6.1"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
"version": "2.29.1"
},
"oclazyload": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz",
"integrity": "sha1-Kjirv/QJDAihEBZxkZRbWfLoJ5w="
"version": "0.6.3"
},
"require-yaml": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/require-yaml/-/require-yaml-0.0.1.tgz",
"integrity": "sha1-LhsY2RPDuqcqWk03O28Tjd0sMr0=",
"requires": {
"js-yaml": "^4.1.0"
"js-yaml": ""
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"version": "2.0.1"
},
"js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": {
"argparse": "^2.0.1"
}
@ -120,14 +245,10 @@
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
"version": "1.0.3"
},
"validator": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-6.3.0.tgz",
"integrity": "sha1-R84j7Y1Ord+p1LjvAHG2zxB418g="
"version": "6.3.0"
}
}
}

View File

@ -131,5 +131,7 @@
"Fichadas impares": "Odd signs",
"Descanso diario 9h.": "Daily rest 9h.",
"Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.",
"Password does not meet requirements": "Password does not meet requirements"
"Password does not meet requirements": "Password does not meet requirements",
"You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies",
"Not enough privileges to edit a client": "Not enough privileges to edit a client"
}

View File

@ -96,7 +96,7 @@
"This postcode already exists": "Este código postal ya existe",
"Concept cannot be blank": "El concepto no puede quedar en blanco",
"File doesn't exists": "El archivo no existe",
"You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies": "No tienes permisos para cambiar la zona o para esos parámetros hay más de una opción de envío, hable con las agencias",
"You don't have privileges to change the zone": "No tienes permisos para cambiar la zona o para esos parámetros hay más de una opción de envío, hable con las agencias",
"This ticket is already on weekly tickets": "Este ticket ya está en tickets programados",
"Ticket id cannot be blank": "El id de ticket no puede quedar en blanco",
"Weekday cannot be blank": "El día de la semana no puede quedar en blanco",
@ -232,5 +232,8 @@
"Fichadas impares": "Fichadas impares",
"Descanso diario 12h.": "Descanso diario 12h.",
"Descanso semanal 36h. / 72h.": "Descanso semanal 36h. / 72h.",
"Dirección incorrecta": "Dirección incorrecta"
}
"Dirección incorrecta": "Dirección incorrecta",
"Modifiable user details only by an administrator": "Detalles de usuario modificables solo por un administrador",
"Modifiable password only via recovery or by an administrator": "Contraseña modificable solo a través de la recuperación o por un administrador",
"Not enough privileges to edit a client": "No tienes suficientes privilegios para editar un cliente"
}

View File

@ -11,14 +11,6 @@
translate>
Delete
</vn-item>
<vn-item
ng-click="$ctrl.onChangeRole()"
name="changeRole"
vn-acl="hr"
vn-acl-action="remove"
translate>
Change role
</vn-item>
<vn-item
ng-if="::$root.user.id == $ctrl.id"
ng-click="$ctrl.onChangePassClick(true)"
@ -79,7 +71,7 @@
label="User"
value="{{$ctrl.user.name}}">
</vn-label-value>
<vn-label-value
<vn-label-value
label="Role"
value="{{$ctrl.user.role.name}}">
</vn-label-value>
@ -128,22 +120,6 @@
question="Are you sure you want to continue?"
message="User will be deactivated">
</vn-confirm>
<vn-dialog
vn-id="changeRole"
on-accept="$ctrl.onChangeRoleAccept()">
<tpl-body>
<vn-autocomplete
label="Role"
ng-model="$ctrl.newRole"
url="Roles"
vn-focus>
</vn-autocomplete>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Accept</button>
</tpl-buttons>
</vn-dialog>
<vn-dialog
vn-id="changePass"
on-accept="$ctrl.onPassChange()"
@ -176,4 +152,4 @@
</vn-dialog>
<vn-popup vn-id="summary">
<vn-user-summary user="$ctrl.user"></vn-user-summary>
</vn-popup>
</vn-popup>

View File

@ -30,20 +30,6 @@ class Controller extends Descriptor {
.then(() => this.vnApp.showSuccess(this.$t('User removed')));
}
onChangeRole() {
this.newRole = this.user.role.id;
this.$.changeRole.show();
}
onChangeRoleAccept() {
const params = {roleFk: this.newRole};
return this.$http.patch(`Accounts/${this.id}`, params)
.then(() => {
this.emit('change');
this.vnApp.showSuccess(this.$t('Role changed succesfully!'));
});
}
onChangePassClick(askOldPass) {
this.$http.get('UserPasswords/findOne')
.then(res => {

View File

@ -30,17 +30,6 @@ describe('component vnUserDescriptor', () => {
});
});
describe('onChangeRoleAccept()', () => {
it('should call backend method to change role', () => {
$httpBackend.expectPATCH('Accounts/1').respond();
controller.onChangeRoleAccept();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.emit).toHaveBeenCalledWith('change');
});
});
describe('onPassChange()', () => {
it('should throw an error when password is empty', () => {
expect(() => {

View File

@ -18,3 +18,4 @@ import './roles';
import './ldap';
import './samba';
import './accounts';
import './privileges';

View File

@ -0,0 +1,42 @@
<mg-ajax path="Accounts/{{post.params.id}}/privileges" options="vnPost"></mg-ajax>
<vn-watcher
vn-id="watcher"
url="Accounts"
data="$ctrl.user"
id-value="$ctrl.$params.id"
form="form"
save="post">
</vn-watcher>
<form
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg" vn-focus>
<vn-vertical>
<vn-check
label="Has grant"
ng-model="$ctrl.user.hasGrant">
</vn-check>
</vn-vertical>
<vn-vertical
class="vn-mt-md">
<vn-autocomplete
label="Role"
ng-model="$ctrl.user.roleFk"
url="Roles">
</vn-autocomplete>
</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>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {}
ngModule.component('vnUserPrivileges', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,2 @@
Privileges: Privilegios
Has grant: Tiene privilegios

View File

@ -19,7 +19,8 @@
{"state": "account.card.basicData", "icon": "settings"},
{"state": "account.card.roles", "icon": "group"},
{"state": "account.card.mailForwarding", "icon": "forward"},
{"state": "account.card.aliases", "icon": "email"}
{"state": "account.card.aliases", "icon": "email"},
{"state": "account.card.privileges", "icon": "badge"}
],
"role": [
{"state": "account.role.card.basicData", "icon": "settings"},
@ -99,6 +100,13 @@
"description": "Mail aliases",
"acl": ["marketing", "hr"]
},
{
"url": "/privileges",
"state": "account.card.privileges",
"component": "vn-user-privileges",
"description": "Privileges",
"acl": ["hr"]
},
{
"url": "/role?q",
"state": "account.role",
@ -249,4 +257,4 @@
"acl": ["developer"]
}
]
}
}

View File

@ -1,8 +1,9 @@
const {Report, Email, smtp} = require('vn-print');
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('claimPickupEmail', {
description: 'Sends the the claim pickup order email with an attached PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
@ -40,7 +41,7 @@ module.exports = Self => {
}
});
Self.claimPickupEmail = async(ctx, id) => {
Self.claimPickupEmail = async ctx => {
const args = Object.assign({}, ctx.args);
const params = {
recipient: args.recipient,

View File

@ -18,46 +18,46 @@ module.exports = Self => {
Self.consumptionSendQueued = async() => {
const queues = await Self.rawSql(`
SELECT
ccq.id,
c.id AS clientFk,
c.email AS clientEmail,
eu.email salesPersonEmail,
REPLACE(json_extract(params, '$.from'), '"', '') AS fromDate,
REPLACE(json_extract(params, '$.to'), '"', '') AS toDate
FROM clientConsumptionQueue ccq
JOIN client c ON (
JSON_SEARCH(
JSON_ARRAY(
json_extract(params, '$.clients')
)
, 'all', c.id) IS NOT NULL)
JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
JOIN ticket t ON t.clientFk = c.id
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
JOIN itemType it ON it.id = i.typeFk
WHERE status = ''
AND it.isPackaging = FALSE
AND DATE(t.shipped) BETWEEN
REPLACE(json_extract(params, '$.from'), '"', '') AND
REPLACE(json_extract(params, '$.to'), '"', '')
GROUP BY c.id`);
id,
params
FROM clientConsumptionQueue
WHERE status = ''`);
for (const queue of queues) {
try {
const args = {
id: queue.clientFk,
recipient: queue.clientEmail,
replyTo: queue.salesPersonEmail,
from: queue.fromDate,
to: queue.toDate
};
const params = JSON.parse(queue.params);
const email = new Email('campaign-metrics', args);
await email.send();
const clients = await Self.rawSql(`
SELECT
c.id AS clientFk,
c.email AS clientEmail,
eu.email salesPersonEmail
FROM client c
JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
JOIN ticket t ON t.clientFk = c.id
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
JOIN itemType it ON it.id = i.typeFk
WHERE c.id IN(?)
AND it.isPackaging = FALSE
AND DATE(t.shipped) BETWEEN ? AND ?
GROUP BY c.id`, [params.clients, params.from, params.to]);
for (const client of clients) {
const args = {
id: client.clientFk,
recipient: client.clientEmail,
replyTo: client.salesPersonEmail,
from: params.from,
to: params.to
};
const email = new Email('campaign-metrics', args);
await email.send();
}
await Self.rawSql(`
UPDATE clientConsumptionQueue
UPDATE clientConsumptionQueue
SET status = 'printed',
printed = ?
WHERE id = ?`,
@ -69,7 +69,7 @@ module.exports = Self => {
WHERE id = ?`,
[error.message, queue.id]);
throw e;
throw error;
}
}

View File

@ -1,5 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('setPassword', {
Self.remoteMethodCtx('setPassword', {
description: 'Sets the password of a non-worker client',
accepts: [
{
@ -20,13 +21,21 @@ module.exports = Self => {
}
});
Self.setPassword = async function(id, newPassword) {
Self.setPassword = async function(ctx, id, newPassword) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const isWorker = await models.Worker.findById(id);
if (isWorker)
throw new Error(`Can't change the password of another worker`);
const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson');
await models.Account.setPassword(id, newPassword);
if (!isSalesPerson)
throw new UserError(`Not enough privileges to edit a client`);
const isClient = await models.Client.findById(id, null);
const isUserAccount = await models.UserAccount.findById(id, null);
if (isClient && !isUserAccount)
await models.Account.setPassword(id, newPassword);
else
throw new UserError(`Modifiable password only via recovery or by an administrator`);
};
};

View File

@ -1,23 +1,43 @@
const models = require('vn-loopback/server/server').models;
describe('Client setPassword', () => {
it('should throw an error the setPassword target is not just a client but a worker', async() => {
let error;
const salesPersonId = 19;
const ctx = {
req: {accessToken: {userId: salesPersonId}}
};
it(`should throw an error if you don't have enough permissions`, async() => {
let error;
const employeeId = 1;
const ctx = {
req: {accessToken: {userId: employeeId}}
};
try {
await models.Client.setPassword(1106, 'newPass?');
await models.Client.setPassword(ctx, 1, 't0pl3v3l.p455w0rd!');
} catch (e) {
error = e;
}
expect(error.message).toEqual(`Can't change the password of another worker`);
expect(error.message).toEqual(`Not enough privileges to edit a client`);
});
it('should throw an error the setPassword target is not just a client but a worker', async() => {
let error;
try {
await models.Client.setPassword(ctx, 1, 't0pl3v3l.p455w0rd!');
} catch (e) {
error = e;
}
expect(error.message).toEqual(`Modifiable password only via recovery or by an administrator`);
});
it('should change the password of the client', async() => {
let error;
try {
await models.Client.setPassword(1101, 't0pl3v3l.p455w0rd!');
await models.Client.setPassword(ctx, 1101, 't0pl3v3l.p455w0rd!');
} catch (e) {
error = e;
}

View File

@ -10,8 +10,9 @@ describe('Client updateUser', () => {
}
}
};
const salesPersonId = 19;
const ctx = {
req: {accessToken: {userId: employeeId}},
req: {accessToken: {userId: salesPersonId}},
args: {name: 'test', active: true}
};
@ -21,8 +22,13 @@ describe('Client updateUser', () => {
});
});
it('should throw an error the target user is not just a client but a worker', async() => {
it(`should throw an error if you don't have enough permissions`, async() => {
let error;
const employeeId = 1;
const ctx = {
req: {accessToken: {userId: employeeId}},
args: {name: 'test', active: true}
};
try {
const clientID = 1106;
await models.Client.updateUser(ctx, clientID);
@ -30,7 +36,19 @@ describe('Client updateUser', () => {
error = e;
}
expect(error.message).toEqual(`Can't update the user details of another worker`);
expect(error.message).toEqual(`Not enough privileges to edit a client`);
});
it('should throw an error the target user is not just a client but a worker', async() => {
let error;
try {
const clientID = 1;
await models.Client.updateUser(ctx, clientID);
} catch (e) {
error = e;
}
expect(error.message).toEqual(`Modifiable user details only by an administrator`);
});
it('should update the user data', async() => {

View File

@ -1,3 +1,4 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('updateUser', {
description: 'Updates the user information',
@ -5,8 +6,7 @@ module.exports = Self => {
{
arg: 'id',
type: 'number',
description: 'The user id',
http: {source: 'path'}
description: 'The user id'
},
{
arg: 'name',
@ -15,7 +15,7 @@ module.exports = Self => {
},
{
arg: 'email',
type: 'string',
type: 'any',
description: 'the user email'
},
{
@ -32,6 +32,7 @@ module.exports = Self => {
Self.updateUser = async function(ctx, id, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
let tx;
const myOptions = {};
@ -44,13 +45,19 @@ module.exports = Self => {
}
try {
const isWorker = await models.Worker.findById(id, null, myOptions);
if (isWorker)
throw new Error(`Can't update the user details of another worker`);
const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson', myOptions);
const user = await models.Account.findById(id, null, myOptions);
if (!isSalesPerson)
throw new UserError(`Not enough privileges to edit a client`);
await user.updateAttributes(ctx.args, myOptions);
const isClient = await models.Client.findById(id, null, myOptions);
const isUserAccount = await models.UserAccount.findById(id, null, myOptions);
if (isClient && !isUserAccount) {
const user = await models.Account.findById(id, null, myOptions);
await user.updateAttributes(ctx.args, myOptions);
} else
throw new UserError(`Modifiable user details only by an administrator`);
if (tx) await tx.commit();
} catch (e) {

View File

@ -77,15 +77,16 @@ export default class Controller extends Section {
onSendClientConsumption() {
const clientIds = this.checked.map(client => client.id);
const params = {
const data = {
clients: clientIds,
from: this.campaign.from,
to: this.campaign.to
};
const params = JSON.stringify(data);
this.$http.post('ClientConsumptionQueues', {params})
.then(() => this.$.filters.hide())
.then(() => this.vnApp.showSuccess(this.$t('Notifications sent!')));
.then(() => this.vnApp.showSuccess(this.$t('Notification sent!')));
}
exprBuilder(param, value) {

View File

@ -69,15 +69,16 @@ describe('Client notification', () => {
data[0].$checked = true;
data[1].$checked = true;
const params = Object.assign({
const args = Object.assign({
clients: [1101, 1102]
}, controller.campaign);
const params = JSON.stringify(args);
$httpBackend.expect('POST', `ClientConsumptionQueues`, {params}).respond(200, params);
controller.onSendClientConsumption();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Notifications sent!');
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Notification sent!');
});
});

View File

@ -143,9 +143,6 @@
},
"packingShelve": {
"type": "number"
},
"weightByPiece": {
"type": "number"
}
},
"relations": {

View File

@ -27,7 +27,7 @@ class Controller extends Section {
if (this.$params.warehouseFk)
this.warehouseFk = this.$params.warehouseFk;
else if (value)
this.warehouseFk = value.itemType.warehouseFk;
this.warehouseFk = this.vnConfig.warehouseFk;
if (this.$params.lineFk)
this.lineFk = this.$params.lineFk;

View File

@ -19,7 +19,8 @@ describe('Item', () => {
describe('set item()', () => {
it('should set warehouseFk property based on itemType warehouseFk', () => {
jest.spyOn(controller.$, '$applyAsync');
controller.item = {id: 1, itemType: {warehouseFk: 1}};
controller.item = {id: 1};
controller.vnConfig = {warehouseFk: 1};
expect(controller.$.$applyAsync).toHaveBeenCalledWith(jasmine.any(Function));
$scope.$apply();

View File

@ -3,6 +3,7 @@ const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('driverRouteEmail', {
description: 'Sends the driver route email with an attached PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',

View File

@ -49,7 +49,8 @@ module.exports = Self => {
a.city,
am.name AS agencyModeName,
u.nickname AS userNickname,
vn.ticketTotalVolume(t.id) AS volume
vn.ticketTotalVolume(t.id) AS volume,
tob.description
FROM route r
JOIN ticket t ON t.routeFk = r.id
LEFT JOIN ticketState ts ON ts.ticketFk = t.id

View File

@ -48,6 +48,9 @@
"description": {
"type": "string"
},
"isOk": {
"type": "boolean"
},
"commissionWorkCenterFk": {
"type": "number"
}

View File

@ -68,6 +68,11 @@
label="Hour finished"
ng-model="$ctrl.route.finished">
</vn-input-time>
<vn-check
class="vn-mr-md"
label="Is served"
ng-model="$ctrl.route.isOk">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-textArea

View File

@ -5,3 +5,4 @@ Km end: Km de fin
Description: Descripción
Hour started: Hora inicio
Hour finished: Hora fin
Is served: Se ha servido

View File

@ -18,7 +18,8 @@ class Controller extends ModuleCard {
'started',
'finished',
'cost',
'zoneFk'
'zoneFk',
'isOk'
],
include: [
{

View File

@ -22,6 +22,8 @@ class Controller extends Descriptor {
recipient: workerUser.emailUser.email,
id: this.id
});
const params = {isOk: true};
return this.$http.patch(`Routes/${this.id}`, params);
}
updateVolume() {

View File

@ -1,69 +1,164 @@
<vn-auto-search
model="model">
</vn-auto-search>
<vn-data-viewer
model="model"
class="vn-w-lg vn-mb-xl">
<div class="vn-w-xl">
<vn-card>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th field="id" number>Id</vn-th>
<vn-th th-id="worker">Worker</vn-th>
<vn-th th-id="agency">Agency</vn-th>
<vn-th th-id="vehicle">Vehicle</vn-th>
<vn-th th-id="created" shrink-date>Date</vn-th>
<vn-th th-id="m3" number></vn-th>
<vn-th th-id="description">Description</vn-th>
<vn-th shrink></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="route in model.data"
class="clickable vn-tr search-result"
ui-sref="route.card.summary({id: {{::route.id}}})"
ng-attr-id="{{::route.id}}" vn-droppable="$ctrl.onDrop($event)">
<vn-td shrink>
<vn-check
ng-model="route.checked"
vn-click-stop>
</vn-check>
</vn-td>
<vn-td number>{{::route.id | dashIfEmpty}}</vn-td>
<vn-td expand>
<span
class="link"
vn-click-stop="workerDescriptor.show($event, route.workerFk)">
{{::route.workerUserName}}
</span>
</vn-td>
<vn-td>{{::route.agencyName | dashIfEmpty}}</vn-td>
<vn-td>{{::route.vehiclePlateNumber | dashIfEmpty}}</vn-td>
<vn-td shrink-date>{{::route.created | dashIfEmpty | date:'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::route.m3 | dashIfEmpty}}</vn-td>
<vn-td>{{::route.description | dashIfEmpty}}</vn-td>
<vn-td>
<vn-icon-button
vn-click-stop="$ctrl.showTicketPopup(route)"
vn-tooltip="Añadir tickets"
icon="icon-ticketAdd">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(route)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-td>
</a>
</vn-tbody>
</vn-table>
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions>
<section class="header">
<vn-tool-bar class="vn-mb-md">
<vn-button
icon="icon-clone"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.openClonationDialog()"
vn-tooltip="Clone selected routes">
</vn-button>
<vn-button
icon="cloud_download"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.showRouteReport()"
vn-tooltip="Download selected routes as PDF">
</vn-button>
<vn-button
icon="check"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.markAsServed()"
vn-tooltip="Mark as served">
</vn-button>
</section>
</slot-actions>
<slot-table>
<table model="model">
<thead>
<tr>
<th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</th>
<th field="id" number>
<span translate>Id</span>
</th>
<th field="workerFk">
<span translate>Worker</span>
</th>
<th field="agencyName">
<span translate>Agency</span>
</th>
<th field="vehiclePlateNumber">
<span translate>Vehicle</span>
</th>
<th field="created" shrink-date>
<span translate>Date</span>
</th>
<th field="m3" number>
<span translate></span>
</th>
<th field="description">
<span translate>Description</span>
</th>
<th field="started">
<span translate>Hour started</span>
</th>
<th field="finished">
<span translate>Hour finished</span>
</th>
<th shrink></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="route in model.data"
class="clickable vn-tr search-result"
ng-attr-id="{{::route.id}}" vn-droppable="$ctrl.onDrop($event)">
<td shrink>
<vn-check
ng-model="route.checked"
vn-click-stop>
</vn-check>
</td>
<td number>{{::route.id | dashIfEmpty}}</td>
<td>
<vn-autocomplete
ng-model="route.workerFk"
url="Workers/activeWithInheritedRole"
show-field="nickname"
search-function="{firstName: $search}"
value-field="id"
where="{role: 'employee'}"
on-change="$ctrl.updateAttributes(route)"
vn-click-stop>
<tpl-item>
<div>{{name}} - {{nickname}}</div>
</tpl-item>
</vn-autocomplete>
</td>
<td expand>
<vn-autocomplete
ng-model="route.agencyModeFk"
url="AgencyModes"
show-field="name"
value-field="id"
on-change="$ctrl.updateAttributes(route)"
vn-click-stop>
</vn-autocomplete>
</td>
<td expand>
<vn-autocomplete
ng-model="route.vehicleFk"
url="Vehicles"
show-field="numberPlate"
value-field="id"
on-change="$ctrl.updateAttributes(route)"
vn-click-stop>
</vn-autocomplete>
</td >
<td>
<vn-date-picker
ng-model="route.created"
on-change="$ctrl.updateAttributes(route)">
</vn-horizontal>
</td>
<td number>{{::route.m3 | dashIfEmpty}}</td>
<td>
<vn-textfield
ng-model="route.description"
on-change="$ctrl.updateAttributes(route)">
</vn-textfield>
</td>
<td expand>
<vn-input-time
ng-model="route.started"
on-change="$ctrl.updateAttributes(route)">
</vn-input-time>
</td>
<td expand>
<vn-input-time
ng-model="route.finished"
on-change="$ctrl.updateAttributes(route)">
</vn-input-time>
</td>
<td>
<vn-icon-button
vn-click-stop="$ctrl.showTicketPopup(route)"
vn-tooltip="Añadir tickets"
icon="icon-ticketAdd">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(route)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
</vn-data-viewer>
</div>
<vn-popup vn-id="summary">
<vn-route-summary
@ -88,20 +183,6 @@
<div fixed-bottom-right>
<vn-vertical style="align-items: center;">
<vn-button class="round sm vn-mb-sm"
icon="icon-clone"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.openClonationDialog()"
vn-tooltip="Clone selected routes"
tooltip-position="left">
</vn-button>
<vn-button class="round sm vn-mb-sm"
icon="cloud_download"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.showRouteReport()"
vn-tooltip="Download selected routes as PDF"
tooltip-position="left">
</vn-button>
<a ui-sref="route.create" vn-bind="+">
<vn-button class="round md vn-mb-sm"
icon="add"

View File

@ -102,6 +102,36 @@ export default class Controller extends Section {
throw error;
});
}
updateAttributes(route) {
if (route.started == null || route.finished == null)
return this.vnApp.showError(this.$t('You must select a valid time'));
if (route.created == null)
return this.vnApp.showError(this.$t('You must select a valid date'));
const params = {
workerFk: route.workerFk,
agencyModeFk: route.agencyModeFk,
vehicleFk: route.vehicleFk,
created: route.created,
description: route.description,
started: route.started,
finished: route.finished
};
const query = `Routes/${route.id}/`;
this.$http.patch(query, params).then(res => {
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
markAsServed() {
const routes = [];
for (let route of this.checked)
routes.push(route.id);
const params = {isOk: true};
for (let routeId of routes)
this.$http.patch(`Routes/${routeId}`, params);
}
}
Controller.$inject = ['$element', '$scope', 'vnReport'];

View File

@ -12,7 +12,7 @@ describe('Component vnRouteIndex', () => {
const $element = angular.element('<vn-route-index></vn-route-index>');
controller = $componentController('vnRouteIndex', {$element});
controller.$.model = crudModel;
controller.$.model.data = [{id: 1}, {id: 2}, {id: 3}];
controller.$.model.data = [{id: 1, checked: true}, {id: 2}, {id: 3}];
}));
describe('checked() getter', () => {
@ -148,4 +148,13 @@ describe('Component vnRouteIndex', () => {
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
describe('markAsServed()', () => {
it('should perform a HTTP patch query', () => {
const data = {isOk: true};
$httpBackend.expect('PATCH', `Routes/1`, data).respond();
controller.markAsServed();
$httpBackend.flush();
});
});
});

View File

@ -2,4 +2,10 @@ Vehicle: Vehículo
Download selected routes as PDF: Descargar rutas seleccionadas como PDF
Clone selected routes: Clonar rutas seleccionadas
The date can't be empty: La fecha no puede estar vacía
Starting date: Fecha de inicio
Starting date: Fecha de inicio
Hour started: Hora inicio
Hour finished: Hora fin
Go to route: Ir a la ruta
You must select a valid time: Debe seleccionar una hora válida
You must select a valid date: Debe seleccionar una fecha válida
Mark as served: Marcar como servidas

View File

@ -11,7 +11,8 @@
<form
class="vn-w-xl"
name="form">
<vn-card class="vn-pa-lg">
<vn-card>
<section class="vn-pa-md">
<vn-tool-bar>
<vn-button
icon="icon-wand"
@ -24,10 +25,19 @@
vn-tooltip="Open buscaman"
icon="icon-buscaman">
</vn-button>
<vn-button
disabled="!$ctrl.isChecked"
ng-click="$ctrl.deletePriority()"
vn-tooltip="Delete priority"
icon="filter_alt_off">
</vn-button>
<vn-button
ng-click="$ctrl.setOrderedPriority($ctrl.tickets)"
vn-tooltip="Renumber all tickets in the order you see on the screen"
icon="format_list_numbered">
</vn-button>
</vn-tool-bar>
</vn-card>
<vn-card class="vn-mt-lg">
<vn-table model="model" auto-load="false" vn-droppable="$ctrl.onDrop($event)">
<vn-table class="vn-pt-md" model="model" auto-load="false" vn-droppable="$ctrl.onDrop($event)">
<vn-thead>
<vn-tr>
<vn-th shrink>
@ -35,6 +45,7 @@
model="model">
</vn-multi-check>
</vn-th>
<vn-th shrink></vn-th>
<vn-th field="priority">Order</vn-th>
<vn-th field="street" expand>Street</vn-th>
<vn-th field="city">City</vn-th>
@ -45,6 +56,7 @@
<vn-th field="id" number>Ticket</vn-th>
<vn-th shrink></vn-th>
<vn-th shrink></vn-th>
<vn-th shrink></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
@ -54,13 +66,20 @@
ng-model="ticket.checked">
</vn-check>
</vn-td>
<vn-td>
<vn-icon-button
icon="low_priority"
ng-click="$ctrl.setHighestPriority(ticket)"
vn-tooltip="Assign highest priority"
tabindex="-1">
</vn-icon-button>
</vn-td>
<vn-td>
<vn-input-number
on-change="$ctrl.setPriority(ticket.id, ticket.priority)"
ng-model="ticket.priority"
rule="Ticket"
class="dense"
display-controls=true>
class="dense">
</vn-input-number>
</vn-td>
<vn-td expand title="{{::ticket.street}}">{{::ticket.street}}</vn-td>
@ -98,9 +117,18 @@
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
<vn-td>
<vn-icon-button
ng-if="::ticket.description"
vn-tooltip="{{::ticket.description}}"
icon="icon-notes"
tabindex="-1">
</vn-icon-button>
</vn-td>
</a>
</vn-tbody>
</vn-table>
</section>
</vn-card>
</form>
</vn-data-viewer>

View File

@ -20,15 +20,48 @@ class Controller extends Section {
return highestPriority + 1;
}
setHighestPriority(ticket) {
const highestPriority = this.getHighestPriority();
if (highestPriority - 1 != ticket.priority) {
const params = {priority: highestPriority};
const query = `Tickets/${ticket.id}/`;
this.$http.patch(query, params).then(res => {
ticket.priority = res.data.priority;
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
}
setPriority(id, priority) {
let params = {priority: priority};
let query = `Tickets/${id}/`;
this.$http.patch(query, params).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$.model.refresh();
});
}
deletePriority() {
const lines = this.getSelectedItems(this.tickets);
for (const line of lines) {
this.$http.patch(`Tickets/${line.id}/`, {priority: null}).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$.model.refresh();
});
}
}
setOrderedPriority(lines) {
let priority = 1;
for (const line of lines) {
this.$http.patch(`Tickets/${line.id}/`, {priority: priority}).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$.model.refresh();
});
priority++;
}
}
getSelectedItems(items) {
const selectedItems = [];
@ -57,8 +90,10 @@ class Controller extends Section {
let lines = this.getSelectedItems(this.tickets);
let url = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=';
lines.forEach(line => {
addresses = addresses + '+to:' + line.postalCode + ' ' + line.city + ' ' + line.street;
lines.forEach((line, index) => {
const previusLine = lines[index - 1] ? lines[index - 1].street : null;
if (previusLine != line.street)
addresses = addresses + '+to:' + line.postalCode + ' ' + line.city + ' ' + line.street;
});
window.open(url + addresses, '_blank');

View File

@ -58,9 +58,23 @@ describe('Route', () => {
});
});
describe('setHighestPriority()', () => {
it('should set a ticket highest priority', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$.model.data = [{priority: 3}];
const ticket = {id: 1, priority: 2};
const res = {data: {priority: 4}};
$httpBackend.expectPATCH(`Tickets/${ticket.id}/`).respond(res);
controller.setHighestPriority(ticket);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('setPriority()', () => {
it('should set a ticket priority', () => {
jest.spyOn(controller.$.model, 'refresh');
jest.spyOn(controller.vnApp, 'showSuccess');
const ticketId = 1;
const priority = 999;
@ -69,6 +83,35 @@ describe('Route', () => {
controller.setPriority(ticketId, priority);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('deletePriority()', () => {
it('should delete priority of all tickets', () => {
jest.spyOn(controller.$.model, 'refresh');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.tickets = [{id: 1, checked: true}];
$httpBackend.expectPATCH(`Tickets/${controller.tickets[0].id}/`).respond();
controller.deletePriority();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
describe('setOrderedPriority()', () => {
it('should set priority of all tickets starting by 1', () => {
jest.spyOn(controller.$.model, 'refresh');
jest.spyOn(controller.vnApp, 'showSuccess');
const tickets = [{id: 1, checked: true}];
$httpBackend.expectPATCH(`Tickets/${tickets[0].id}/`).respond();
controller.setOrderedPriority(tickets);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});

View File

@ -13,3 +13,6 @@ The route's vehicle doesn't have a delivery point: El vehículo de la ruta no ti
The route doesn't have a vehicle: La ruta no tiene un vehículo
Population: Población
Unlink selected zone?: Desvincular zona seleccionada?
Delete priority: Borrar orden
Renumber all tickets in the order you see on the screen: Renumerar todos los tickets con el orden que ves por pantalla
Assign highest priority: Asignar máxima prioridad

View File

@ -0,0 +1,57 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethodCtx('getItemTypeWorker', {
description: 'Returns the workers that appear in itemType',
accessType: 'READ',
accepts: [{
arg: 'filter',
type: 'Object',
description: 'Filter defining where and paginated data',
required: true
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/getItemTypeWorker`,
verb: 'GET'
}
});
Self.getItemTypeWorker = async(ctx, filter, options) => {
const myOptions = {};
const conn = Self.dataSource.connector;
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
const query =
`SELECT DISTINCT u.id, u.nickname
FROM itemType it
JOIN worker w ON w.id = it.workerFk
JOIN account.user u ON u.id = w.id`;
let stmt = new ParameterizedSQL(query);
if (filter.where) {
const value = filter.where.firstName;
const myFilter = {
where: {or: [
{'w.firstName': {like: `%${value}%`}},
{'w.lastName': {like: `%${value}%`}},
{'u.name': {like: `%${value}%`}},
{'u.nickname': {like: `%${value}%`}}
]}
};
stmt.merge(conn.makeSuffix(myFilter));
}
if (tx) await tx.commit();
return conn.executeStmt(stmt);
};
};

View File

@ -0,0 +1,21 @@
const models = require('vn-loopback/server/server').models;
describe('ticket-request getItemTypeWorker()', () => {
const ctx = {req: {accessToken: {userId: 18}}};
it('should return the buyer as result', async() => {
const filter = {where: {firstName: 'buyer'}};
const result = await models.TicketRequest.getItemTypeWorker(ctx, filter);
expect(result.length).toEqual(1);
});
it('should return the workers at itemType as result', async() => {
const filter = {};
const result = await models.TicketRequest.getItemTypeWorker(ctx, filter);
expect(result.length).toBeGreaterThan(1);
});
});

View File

@ -18,6 +18,7 @@ module.exports = Self => {
Self.closeAll = async() => {
const toDate = new Date();
toDate.setHours(0, 0, 0, 0);
toDate.setDate(toDate.getDate() - 1);
const todayMinDate = new Date();

View File

@ -126,8 +126,7 @@ module.exports = Self => {
myOptions);
if (!zoneShipped || zoneShipped.zoneFk != args.zoneFk) {
const error = `You don't have privileges to change the zone or
for these parameters there are more than one shipping options, talk to agencies`;
const error = `You don't have privileges to change the zone`;
throw new UserError(error);
}

View File

@ -88,8 +88,7 @@ module.exports = Self => {
myOptions);
if (!zoneShipped || zoneShipped.zoneFk != args.zoneId) {
const error = `You don't have privileges to change the zone or
for these parameters there are more than one shipping options, talk to agencies`;
const error = `You don't have privileges to change the zone`;
throw new UserError(error);
}

View File

@ -5,6 +5,7 @@ module.exports = function(Self) {
require('../methods/ticket-request/filter')(Self);
require('../methods/ticket-request/deny')(Self);
require('../methods/ticket-request/confirm')(Self);
require('../methods/ticket-request/getItemTypeWorker')(Self);
Self.observe('before save', async function(ctx) {
if (ctx.isNewInstance) {

View File

@ -18,9 +18,8 @@
<vn-autocomplete
label="Buyer"
ng-model="$ctrl.ticketRequest.attenderFk"
url="Workers/activeWithRole"
url="TicketRequests/getItemTypeWorker"
show-field="nickname"
where="{role: {inq: ['logistic', 'buyer']}}"
search-function="{firstName: $search}">
</vn-autocomplete>
</vn-horizontal>

View File

@ -42,10 +42,10 @@
<th field="id" shrink>
<span translate>Id</span>
</th>
<th field="cargoSupplierFk" expand>
<th field="cargoSupplierFk">
<span translate>Supplier</span>
</th>
<th field="agencyModeFk" expand>
<th field="agencyModeFk">
<span translate>Agency</span>
</th>
<th field="ref">
@ -100,37 +100,37 @@
{{::travel.id}}
</span>
</td>
<td expand vn-click-stop>
<td class="multi-line" vn-click-stop>
<span
class="link"
ng-click="supplierDescriptor.show($event, travel.cargoSupplierFk)">
{{::travel.cargoSupplierNickname}}
</span>
</td>
<td expand>{{::travel.agencyModeName}}</td>
<td
name="reference"
expand
vn-click-stop>
<vn-textfield
class="dense td-editable"
ng-model="travel.ref"
on-change="$ctrl.save(travel.id, {ref: value})">
</vn-textfield>
</vn-icon>
<td>{{::travel.agencyModeName}}</td>
<td vn-click-stop>
<vn-td-editable name="reference" expand>
<text>{{travel.ref}}</text>
<field>
<vn-textfield class="dense" vn-focus
ng-model="travel.ref"
on-change="$ctrl.save(travel.id, {ref: value})">
</vn-textfield>
</field>
</vn-td-editable>
</td>
<td number>{{::travel.stickers}}</td>
<td
name="lockedKg"
expand
vn-click-stop>
<vn-input-number
number
class="td-editable number"
ng-model="travel.kg"
on-change="$ctrl.save(travel.id, {kg: value})"
min="0">
</vn-input-number>
<td vn-click-stop>
<vn-td-editable name="lockedKg" expand style="text-align: right">
<text number>{{travel.kg}}</text>
<field>
<vn-input-number class="dense" vn-focus
ng-model="travel.kg"
on-change="$ctrl.save(travel.id, {kg: value})"
min="0">
</vn-input-number>
</field>
</vn-td-editable>
</td>
<td number>{{::travel.loadedKg}}</td>
<td number>{{::travel.volumeKg}}</td>
@ -150,7 +150,7 @@
{{::entry.id}}
</span>
</td>
<td>
<td class="multi-line">
<span
class="link"
ng-click="supplierDescriptor.show($event, entry.supplierFk)">
@ -158,7 +158,7 @@
</span>
</td>
<td></td>
<td expand>{{::entry.ref}}</td>
<td class="td-editable">{{::entry.ref}}</td>
<td number>{{::entry.stickers}}</td>
<td number></td>
<td number>{{::entry.loadedkg}}</td>

View File

@ -3,7 +3,6 @@
vn-travel-extra-community {
.header {
margin-bottom: 16px;
font-size: 1.25rem;
line-height: 1;
padding: 7px;
padding-bottom: 7px;
@ -16,6 +15,10 @@ vn-travel-extra-community {
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
.multi-line{
padding-top: 15px;
padding-bottom: 15px;
}
}
table[vn-droppable] {
@ -29,10 +32,10 @@ vn-travel-extra-community {
outline: 0;
height: 65px;
pointer-events: fill;
user-select:all;
user-select: all;
}
tr[draggable] *::selection{
tr[draggable] *::selection {
background-color: transparent;
}
@ -43,16 +46,22 @@ vn-travel-extra-community {
tr[draggable].dragging {
background-color: $color-primary-light;
color: $color-font-light;
font-weight:bold;
font-weight: bold;
}
.td-editable{
input{
font-size: 1.25rem!important;
}
.multi-line{
max-width: 200px;
word-wrap: normal;
white-space: normal;
}
.number *{
text-align: right;
vn-td-editable text {
background-color: transparent;
padding: 0;
border: 0;
border-bottom: 1px dashed $color-active;
border-radius: 0;
color: $color-active
}
}

View File

@ -59,7 +59,10 @@ module.exports = Self => {
}
const leaves = map.get(parentId);
setLeaves(leaves);
const maxNodes = 250;
if (res.length <= maxNodes)
setLeaves(leaves);
return leaves || [];
};

44185
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@ class Report extends Component {
});
const page = (await browser.pages())[0];
await page.emulateMedia('screen');
await page.emulateMediaType('screen');
await page.setContent(template);
const element = await page.$('#pageFooter');

3290
print/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff