4077-login_recover-password & account_verifyEmail #1063

Merged
alexm merged 52 commits from 4077-login_recover-password into dev 2022-11-28 11:34:03 +00:00
63 changed files with 48591 additions and 340 deletions
Showing only changes of commit 5c4daee35e - Show all commits

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

@ -12,6 +12,7 @@ module.exports = Self => {
require('../methods/account/set-password')(Self); require('../methods/account/set-password')(Self);
require('../methods/account/recover-password')(Self); require('../methods/account/recover-password')(Self);
require('../methods/account/validate-token')(Self); require('../methods/account/validate-token')(Self);
require('../methods/account/privileges')(Self);
// Validations // Validations

View File

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

View File

@ -7,13 +7,18 @@ process.on('warning', warning => {
console.log(warning.stack); console.log(warning.stack);
}); });
process.on('exit', async function() {
if (container) await container.rm();
});
let container;
async function test() { async function test() {
let isCI = false; let isCI = false;
if (process.argv[2] === 'ci') if (process.argv[2] === 'ci')
isCI = true; isCI = true;
const container = new Docker(); container = new Docker();
await container.run(isCI); await container.run(isCI);
dataSources = JSON.parse(JSON.stringify(dataSources)); dataSources = JSON.parse(JSON.stringify(dataSources));
@ -41,8 +46,6 @@ async function test() {
} }
})); }));
jasmine.exitOnCompletion = false;
if (isCI) { if (isCI) {
const JunitReporter = require('jasmine-reporters'); const JunitReporter = require('jasmine-reporters');
jasmine.addReporter(new JunitReporter.JUnitXmlReporter()); 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. * to avoid a bug with OverlayFS driver on MacOS.
* *
* @param {Boolean} ci continuous integration environment argument * @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 d = new Date();
let pad = v => v < 10 ? '0' + v : v; let pad = v => v < 10 ? '0' + v : v;
let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; 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 runChown = process.platform != 'linux';
let network = ''; let network = '';
if (ci) network = '--network="jenkins"'; if (ci) network = `--network="${networkName}"`;
log('Starting container...'); log('Starting container...');
const container = await this.execP(` const container = await this.execP(`
@ -60,7 +61,7 @@ module.exports = class Docker {
let netSettings = JSON.parse(inspect.stdout); let netSettings = JSON.parse(inspect.stdout);
if (ci) { if (ci) {
this.dbConf.host = netSettings.Networks.jenkins.IPAddress; this.dbConf.host = netSettings.Networks[networkName].IPAddress;
this.dbConf.port = 3306; this.dbConf.port = 3306;
} else } else
this.dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort']; 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`) INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `level`, `wagon`, `smartTagFk`, `usedShelves`, `itemCount`, `liters`)
VALUES VALUES
(9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL); (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: { accountDescriptor: {
menuButton: 'vn-user-descriptor vn-icon-button[icon="more_vert"]', menuButton: 'vn-user-descriptor vn-icon-button[icon="more_vert"]',
deleteAccount: '.vn-menu [name="deleteUser"]', deleteAccount: '.vn-menu [name="deleteUser"]',
changeRole: '.vn-menu [name="changeRole"]',
setPassword: '.vn-menu [name="setPassword"]', setPassword: '.vn-menu [name="setPassword"]',
activateAccount: '.vn-menu [name="enableAccount"]', activateAccount: '.vn-menu [name="enableAccount"]',
activateUser: '.vn-menu [name="activateUser"]', activateUser: '.vn-menu [name="activateUser"]',
deactivateUser: '.vn-menu [name="deactivateUser"]', deactivateUser: '.vn-menu [name="deactivateUser"]',
newPassword: 'vn-textfield[ng-model="$ctrl.newPassword"]', newPassword: 'vn-textfield[ng-model="$ctrl.newPassword"]',
repeatPassword: 'vn-textfield[ng-model="$ctrl.repeatPassword"]', repeatPassword: 'vn-textfield[ng-model="$ctrl.repeatPassword"]',
newRole: 'vn-autocomplete[ng-model="$ctrl.newRole"]',
activeAccountIcon: 'vn-icon[icon="contact_mail"]', activeAccountIcon: 'vn-icon[icon="contact_mail"]',
activeUserIcon: 'vn-icon[icon="icon-disabled"]', activeUserIcon: 'vn-icon[icon="icon-disabled"]',
acceptButton: 'button[response="accept"]', acceptButton: 'button[response="accept"]',
@ -143,6 +141,11 @@ export default {
verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]', verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]',
save: 'vn-account-samba vn-submit' 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: { clientsIndex: {
createClientButton: `vn-float-button` createClientButton: `vn-float-button`
}, },
@ -835,14 +838,16 @@ export default {
confirmButton: '.vn-confirm.shown button[response="accept"]', confirmButton: '.vn-confirm.shown button[response="accept"]',
}, },
routeIndex: { routeIndex: {
anyResult: 'vn-table a', anyResult: 'vn-route-index tbody > tr',
firstRouteCheckbox: 'a:nth-child(1) vn-td:nth-child(1) > vn-check', firstRouteCheckbox: 'vn-route-index tbody > tr:nth-child(1) > td:nth-child(1) > vn-check',
addNewRouteButton: 'vn-route-index a[ui-sref="route.create"]', addNewRouteButton: 'vn-route-index a[ui-sref="route.create"]',
cloneButton: 'vn-route-index button > vn-icon[icon="icon-clone"]', cloneButton: 'vn-route-index button > vn-icon[icon="icon-clone"]',
submitClonationButton: 'tpl-buttons > button[response="accept"]', submitClonationButton: 'tpl-buttons > button[response="accept"]',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]', openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
searchAgencyAutocomlete: 'vn-route-search-panel vn-autocomplete[ng-model="filter.agencyModeFk"]', searchAgencyAutocomlete: 'vn-route-search-panel vn-autocomplete[ng-model="filter.agencyModeFk"]',
advancedSearchButton: 'vn-route-search-panel button[type=submit]', 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: { createRouteView: {
worker: 'vn-route-create vn-autocomplete[ng-model="$ctrl.route.workerFk"]', worker: 'vn-route-create vn-autocomplete[ng-model="$ctrl.route.workerFk"]',
@ -862,6 +867,8 @@ export default {
firstTicketDescriptor: '.vn-popover.shown vn-ticket-descriptor', firstTicketDescriptor: '.vn-popover.shown vn-ticket-descriptor',
firstAlias: 'vn-route-summary vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(3) > span', firstAlias: 'vn-route-summary vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(3) > span',
firstClientDescriptor: '.vn-popover.shown vn-client-descriptor', firstClientDescriptor: '.vn-popover.shown vn-client-descriptor',
goToRouteSummaryButton: 'vn-route-summary > vn-card > h5 > a',
}, },
routeBasicData: { routeBasicData: {
worker: 'vn-route-basic-data vn-autocomplete[ng-model="$ctrl.route.workerFk"]', 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() => { beforeAll(async() => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('employee', 'client'); await page.loginAndModule('salesPerson', 'client');
await page.accessToSearchResult('max'); await page.accessToSearchResult('max');
await page.accessToSection('client.card.webAccess'); await page.accessToSection('client.card.webAccess');
}); });

View File

@ -9,7 +9,8 @@ describe('Route summary path', () => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('employee', 'route'); 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() => { afterAll(async() => {
@ -34,6 +35,8 @@ describe('Route summary path', () => {
}); });
it('should click on the first ticket ID making the descriptor popover visible', async() => { 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.waitToClick(selectors.routeSummary.firstTicketID);
await page.waitForSelector(selectors.routeSummary.firstTicketDescriptor); await page.waitForSelector(selectors.routeSummary.firstTicketDescriptor);
const visible = await page.isVisible(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('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', () => { describe('activate account', () => {
it(`should check the active account icon isn't present in the descriptor`, async() => { it(`should check the active account icon isn't present in the descriptor`, async() => {
await page.waitForNumberOfElements(selectors.accountDescriptor.activeAccountIcon, 0); 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", "name": "salix-front",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "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": { "dependencies": {
"@uirouter/angularjs": { "@uirouter/angularjs": {
"version": "1.0.29", "version": "1.0.29",
"resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.29.tgz",
"integrity": "sha512-RImWnBarNixkMto0o8stEaGwZmvhv5cnuOLXyMU2pY8MP2rgEF74ZNJTLeJCW14LR7XDUxVH8Mk8bPI6lxedmQ==",
"requires": { "requires": {
"@uirouter/core": "6.0.7" "@uirouter/core": "6.0.7"
} }
}, },
"@uirouter/core": { "@uirouter/core": {
"version": "6.0.7", "version": "6.0.7"
"resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.7.tgz",
"integrity": "sha512-KUTJxL+6q0PiBnFx4/Z+Hsyg0pSGiaW5yZQeJmUxknecjpTbnXkLU8H2EqRn9N2B+qDRa7Jg8RcgeNDPY72O1w=="
}, },
"angular": { "angular": {
"version": "1.8.2", "version": "1.8.2"
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.2.tgz",
"integrity": "sha512-IauMOej2xEe7/7Ennahkbb5qd/HFADiNuLSESz9Q27inmi32zB0lnAsFeLEWcox3Gd1F6YhNd1CP7/9IukJ0Gw=="
}, },
"angular-animate": { "angular-animate": {
"version": "1.8.2", "version": "1.8.2"
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz",
"integrity": "sha512-Jbr9+grNMs9Kj57xuBU3Ju3NOPAjS1+g2UAwwDv7su1lt0/PLDy+9zEwDiu8C8xJceoTbmBNKiWGPJGBdCQLlA=="
}, },
"angular-moment": { "angular-moment": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz",
"integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==",
"requires": { "requires": {
"moment": ">=2.8.0 <3.0.0" "moment": ">=2.8.0 <3.0.0"
} }
}, },
"angular-translate": { "angular-translate": {
"version": "2.18.4", "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": { "requires": {
"angular": "^1.8.0" "angular": "^1.8.0"
} }
}, },
"angular-translate-loader-partial": { "angular-translate-loader-partial": {
"version": "2.18.4", "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": { "requires": {
"angular-translate": "~2.18.4" "angular-translate": "~2.18.4"
} }
}, },
"argparse": { "argparse": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
}, },
"croppie": { "croppie": {
"version": "2.6.5", "version": "2.6.5"
"resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.5.tgz",
"integrity": "sha512-IlChnVUGG5T3w2gRZIaQgBtlvyuYnlUWs2YZIXXR3H9KrlO1PtBT3j+ykxvy9eZIWhk+V5SpBmhCQz5UXKrEKQ=="
}, },
"esprima": { "esprima": {
"version": "4.0.1", "version": "4.0.1"
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
}, },
"js-yaml": { "js-yaml": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@ -80,39 +217,27 @@
}, },
"mg-crud": { "mg-crud": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/mg-crud/-/mg-crud-1.1.2.tgz",
"integrity": "sha1-p6AWGzWSPK7/8ZpIBpS2V1vDggw=",
"requires": { "requires": {
"angular": "^1.6.1" "angular": "^1.6.1"
} }
}, },
"moment": { "moment": {
"version": "2.29.1", "version": "2.29.1"
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
}, },
"oclazyload": { "oclazyload": {
"version": "0.6.3", "version": "0.6.3"
"resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz",
"integrity": "sha1-Kjirv/QJDAihEBZxkZRbWfLoJ5w="
}, },
"require-yaml": { "require-yaml": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/require-yaml/-/require-yaml-0.0.1.tgz",
"integrity": "sha1-LhsY2RPDuqcqWk03O28Tjd0sMr0=",
"requires": { "requires": {
"js-yaml": "^4.1.0" "js-yaml": ""
}, },
"dependencies": { "dependencies": {
"argparse": { "argparse": {
"version": "2.0.1", "version": "2.0.1"
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
}, },
"js-yaml": { "js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": { "requires": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
} }
@ -120,14 +245,10 @@
} }
}, },
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3"
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
}, },
"validator": { "validator": {
"version": "6.3.0", "version": "6.3.0"
"resolved": "https://registry.npmjs.org/validator/-/validator-6.3.0.tgz",
"integrity": "sha1-R84j7Y1Ord+p1LjvAHG2zxB418g="
} }
} }
} }

View File

@ -133,5 +133,7 @@
"Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.", "Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.",
"Verify email": "Verify email", "Verify email": "Verify email",
"Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Click on the following link to verify this email. If you haven't requested this email, just ignore it", "Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Click on the following link to verify this email. If you haven't requested this email, just ignore it",
"Password does not meet requirements": "Password does not meet requirements" "Password does not meet requirements": "Password does not meet requirements",
"You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies",
"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", "This postcode already exists": "Este código postal ya existe",
"Concept cannot be blank": "El concepto no puede quedar en blanco", "Concept cannot be blank": "El concepto no puede quedar en blanco",
"File doesn't exists": "El archivo no existe", "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", "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", "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", "Weekday cannot be blank": "El día de la semana no puede quedar en blanco",
@ -238,5 +238,8 @@
"Verify email": "Verificar correo", "Verify email": "Verificar correo",
"Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Pulsa en el siguiente link para verificar este correo. Si no has pedido este correo, simplemente ignóralo.", "Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Pulsa en el siguiente link para verificar este correo. Si no has pedido este correo, simplemente ignóralo.",
"Click on the following link to change your password": "Click on the following link to change your password", "Click on the following link to change your password": "Click on the following link to change your password",
"Landing cannot be lesser than shipment": "Landing cannot be lesser than shipment" "Landing cannot be lesser than shipment": "Landing cannot be lesser than shipment",
"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> translate>
Delete Delete
</vn-item> </vn-item>
<vn-item
ng-click="$ctrl.onChangeRole()"
name="changeRole"
vn-acl="hr"
vn-acl-action="remove"
translate>
Change role
</vn-item>
<vn-item <vn-item
ng-if="::$root.user.id == $ctrl.id" ng-if="::$root.user.id == $ctrl.id"
ng-click="$ctrl.onChangePassClick(true)" ng-click="$ctrl.onChangePassClick(true)"
@ -128,22 +120,6 @@
question="Are you sure you want to continue?" question="Are you sure you want to continue?"
message="User will be deactivated"> message="User will be deactivated">
</vn-confirm> </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-dialog
vn-id="changePass" vn-id="changePass"
on-accept="$ctrl.onPassChange()" on-accept="$ctrl.onPassChange()"

View File

@ -40,20 +40,6 @@ class Controller extends Descriptor {
.then(() => this.vnApp.showSuccess(this.$t('User removed'))); .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) { onChangePassClick(askOldPass) {
this.$http.get('UserPasswords/findOne') this.$http.get('UserPasswords/findOne')
.then(res => { .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()', () => { describe('onPassChange()', () => {
it('should throw an error when password is empty', () => { it('should throw an error when password is empty', () => {
expect(() => { expect(() => {

View File

@ -18,3 +18,4 @@ import './roles';
import './ldap'; import './ldap';
import './samba'; import './samba';
import './accounts'; 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.basicData", "icon": "settings"},
{"state": "account.card.roles", "icon": "group"}, {"state": "account.card.roles", "icon": "group"},
{"state": "account.card.mailForwarding", "icon": "forward"}, {"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": [ "role": [
{"state": "account.role.card.basicData", "icon": "settings"}, {"state": "account.role.card.basicData", "icon": "settings"},
@ -99,6 +100,13 @@
"description": "Mail aliases", "description": "Mail aliases",
"acl": ["marketing", "hr"] "acl": ["marketing", "hr"]
}, },
{
"url": "/privileges",
"state": "account.card.privileges",
"component": "vn-user-privileges",
"description": "Privileges",
"acl": ["hr"]
},
{ {
"url": "/role?q", "url": "/role?q",
"state": "account.role", "state": "account.role",

View File

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

View File

@ -18,43 +18,43 @@ module.exports = Self => {
Self.consumptionSendQueued = async() => { Self.consumptionSendQueued = async() => {
const queues = await Self.rawSql(` const queues = await Self.rawSql(`
SELECT SELECT
ccq.id, id,
c.id AS clientFk, params
c.email AS clientEmail, FROM clientConsumptionQueue
eu.email salesPersonEmail, WHERE status = ''`);
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`);
for (const queue of queues) { for (const queue of queues) {
try { try {
const args = { const params = JSON.parse(queue.params);
id: queue.clientFk,
recipient: queue.clientEmail,
replyTo: queue.salesPersonEmail,
from: queue.fromDate,
to: queue.toDate
};
const email = new Email('campaign-metrics', args); const clients = await Self.rawSql(`
await email.send(); 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(` await Self.rawSql(`
UPDATE clientConsumptionQueue UPDATE clientConsumptionQueue
@ -69,7 +69,7 @@ module.exports = Self => {
WHERE id = ?`, WHERE id = ?`,
[error.message, queue.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 => { module.exports = Self => {
Self.remoteMethod('setPassword', { Self.remoteMethodCtx('setPassword', {
description: 'Sets the password of a non-worker client', description: 'Sets the password of a non-worker client',
accepts: [ 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 models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const isWorker = await models.Worker.findById(id); const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson');
if (isWorker)
throw new Error(`Can't change the password of another worker`);
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; const models = require('vn-loopback/server/server').models;
describe('Client setPassword', () => { describe('Client setPassword', () => {
it('should throw an error the setPassword target is not just a client but a worker', async() => { const salesPersonId = 19;
let error; 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 { try {
await models.Client.setPassword(1106, 'newPass?'); await models.Client.setPassword(ctx, 1, 't0pl3v3l.p455w0rd!');
} catch (e) { } catch (e) {
error = 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() => { it('should change the password of the client', async() => {
let error; let error;
try { try {
await models.Client.setPassword(1101, 't0pl3v3l.p455w0rd!'); await models.Client.setPassword(ctx, 1101, 't0pl3v3l.p455w0rd!');
} catch (e) { } catch (e) {
error = e; error = e;
} }

View File

@ -10,8 +10,9 @@ describe('Client updateUser', () => {
} }
} }
}; };
const salesPersonId = 19;
const ctx = { const ctx = {
req: {accessToken: {userId: employeeId}}, req: {accessToken: {userId: salesPersonId}},
args: {name: 'test', active: true} 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; let error;
const employeeId = 1;
const ctx = {
req: {accessToken: {userId: employeeId}},
args: {name: 'test', active: true}
};
try { try {
const clientID = 1106; const clientID = 1106;
await models.Client.updateUser(ctx, clientID); await models.Client.updateUser(ctx, clientID);
@ -30,7 +36,19 @@ describe('Client updateUser', () => {
error = e; 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() => { it('should update the user data', async() => {

View File

@ -1,3 +1,4 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('updateUser', { Self.remoteMethodCtx('updateUser', {
description: 'Updates the user information', description: 'Updates the user information',
@ -5,8 +6,7 @@ module.exports = Self => {
{ {
arg: 'id', arg: 'id',
type: 'number', type: 'number',
description: 'The user id', description: 'The user id'
http: {source: 'path'}
}, },
{ {
arg: 'name', arg: 'name',
@ -15,7 +15,7 @@ module.exports = Self => {
}, },
{ {
arg: 'email', arg: 'email',
type: 'string', type: 'any',
description: 'the user email' description: 'the user email'
}, },
{ {
@ -32,6 +32,7 @@ module.exports = Self => {
Self.updateUser = async function(ctx, id, options) { Self.updateUser = async function(ctx, id, options) {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
let tx; let tx;
const myOptions = {}; const myOptions = {};
@ -44,13 +45,19 @@ module.exports = Self => {
} }
try { try {
const isWorker = await models.Worker.findById(id, null, myOptions); const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson', myOptions);
if (isWorker)
throw new Error(`Can't update the user details of another worker`);
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(); if (tx) await tx.commit();
} catch (e) { } catch (e) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,6 +102,36 @@ export default class Controller extends Section {
throw error; 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']; Controller.$inject = ['$element', '$scope', 'vnReport'];

View File

@ -12,7 +12,7 @@ describe('Component vnRouteIndex', () => {
const $element = angular.element('<vn-route-index></vn-route-index>'); const $element = angular.element('<vn-route-index></vn-route-index>');
controller = $componentController('vnRouteIndex', {$element}); controller = $componentController('vnRouteIndex', {$element});
controller.$.model = crudModel; 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', () => { describe('checked() getter', () => {
@ -148,4 +148,13 @@ describe('Component vnRouteIndex', () => {
expect(controller.$.model.refresh).toHaveBeenCalledWith(); 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

@ -3,3 +3,9 @@ Download selected routes as PDF: Descargar rutas seleccionadas como PDF
Clone selected routes: Clonar rutas seleccionadas Clone selected routes: Clonar rutas seleccionadas
The date can't be empty: La fecha no puede estar vacía 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 <form
class="vn-w-xl" class="vn-w-xl"
name="form"> name="form">
<vn-card class="vn-pa-lg"> <vn-card>
<section class="vn-pa-md">
<vn-tool-bar> <vn-tool-bar>
<vn-button <vn-button
icon="icon-wand" icon="icon-wand"
@ -24,10 +25,19 @@
vn-tooltip="Open buscaman" vn-tooltip="Open buscaman"
icon="icon-buscaman"> icon="icon-buscaman">
</vn-button> </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-tool-bar>
</vn-card> <vn-table class="vn-pt-md" model="model" auto-load="false" vn-droppable="$ctrl.onDrop($event)">
<vn-card class="vn-mt-lg">
<vn-table model="model" auto-load="false" vn-droppable="$ctrl.onDrop($event)">
<vn-thead> <vn-thead>
<vn-tr> <vn-tr>
<vn-th shrink> <vn-th shrink>
@ -35,6 +45,7 @@
model="model"> model="model">
</vn-multi-check> </vn-multi-check>
</vn-th> </vn-th>
<vn-th shrink></vn-th>
<vn-th field="priority">Order</vn-th> <vn-th field="priority">Order</vn-th>
<vn-th field="street" expand>Street</vn-th> <vn-th field="street" expand>Street</vn-th>
<vn-th field="city">City</vn-th> <vn-th field="city">City</vn-th>
@ -45,6 +56,7 @@
<vn-th field="id" number>Ticket</vn-th> <vn-th field="id" number>Ticket</vn-th>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
<vn-th shrink></vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>
@ -54,13 +66,20 @@
ng-model="ticket.checked"> ng-model="ticket.checked">
</vn-check> </vn-check>
</vn-td> </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-td>
<vn-input-number <vn-input-number
on-change="$ctrl.setPriority(ticket.id, ticket.priority)" on-change="$ctrl.setPriority(ticket.id, ticket.priority)"
ng-model="ticket.priority" ng-model="ticket.priority"
rule="Ticket" rule="Ticket"
class="dense" class="dense">
display-controls=true>
</vn-input-number> </vn-input-number>
</vn-td> </vn-td>
<vn-td expand title="{{::ticket.street}}">{{::ticket.street}}</vn-td> <vn-td expand title="{{::ticket.street}}">{{::ticket.street}}</vn-td>
@ -98,9 +117,18 @@
tabindex="-1"> tabindex="-1">
</vn-icon-button> </vn-icon-button>
</vn-td> </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-tbody>
</vn-table> </vn-table>
</section>
</vn-card> </vn-card>
</form> </form>
</vn-data-viewer> </vn-data-viewer>

View File

@ -20,15 +20,48 @@ class Controller extends Section {
return highestPriority + 1; 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) { setPriority(id, priority) {
let params = {priority: priority}; let params = {priority: priority};
let query = `Tickets/${id}/`; let query = `Tickets/${id}/`;
this.$http.patch(query, params).then(() => { this.$http.patch(query, params).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!')); 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) { getSelectedItems(items) {
const selectedItems = []; const selectedItems = [];
@ -57,8 +90,10 @@ class Controller extends Section {
let lines = this.getSelectedItems(this.tickets); let lines = this.getSelectedItems(this.tickets);
let url = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr='; let url = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=';
lines.forEach(line => { lines.forEach((line, index) => {
addresses = addresses + '+to:' + line.postalCode + ' ' + line.city + ' ' + line.street; 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'); 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()', () => { describe('setPriority()', () => {
it('should set a ticket priority', () => { it('should set a ticket priority', () => {
jest.spyOn(controller.$.model, 'refresh');
jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.vnApp, 'showSuccess');
const ticketId = 1; const ticketId = 1;
const priority = 999; const priority = 999;
@ -69,6 +83,35 @@ describe('Route', () => {
controller.setPriority(ticketId, priority); controller.setPriority(ticketId, priority);
$httpBackend.flush(); $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.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$.model.refresh).toHaveBeenCalledWith(); 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 The route doesn't have a vehicle: La ruta no tiene un vehículo
Population: Población Population: Población
Unlink selected zone?: Desvincular zona seleccionada? 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() => { Self.closeAll = async() => {
const toDate = new Date(); const toDate = new Date();
toDate.setHours(0, 0, 0, 0);
toDate.setDate(toDate.getDate() - 1); toDate.setDate(toDate.getDate() - 1);
const todayMinDate = new Date(); const todayMinDate = new Date();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@
vn-travel-extra-community { vn-travel-extra-community {
.header { .header {
margin-bottom: 16px; margin-bottom: 16px;
font-size: 1.25rem;
line-height: 1; line-height: 1;
padding: 7px; padding: 7px;
padding-bottom: 7px; padding-bottom: 7px;
@ -16,6 +15,10 @@ vn-travel-extra-community {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
cursor: pointer; cursor: pointer;
.multi-line{
padding-top: 15px;
padding-bottom: 15px;
}
} }
table[vn-droppable] { table[vn-droppable] {
@ -29,10 +32,10 @@ vn-travel-extra-community {
outline: 0; outline: 0;
height: 65px; height: 65px;
pointer-events: fill; pointer-events: fill;
user-select:all; user-select: all;
} }
tr[draggable] *::selection{ tr[draggable] *::selection {
background-color: transparent; background-color: transparent;
} }
@ -43,16 +46,22 @@ vn-travel-extra-community {
tr[draggable].dragging { tr[draggable].dragging {
background-color: $color-primary-light; background-color: $color-primary-light;
color: $color-font-light; color: $color-font-light;
font-weight:bold; font-weight: bold;
} }
.td-editable{
input{ .multi-line{
font-size: 1.25rem!important; max-width: 200px;
} word-wrap: normal;
white-space: normal;
} }
.number *{ vn-td-editable text {
text-align: right; 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); const leaves = map.get(parentId);
setLeaves(leaves);
const maxNodes = 250;
if (res.length <= maxNodes)
setLeaves(leaves);
return 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]; const page = (await browser.pages())[0];
await page.emulateMedia('screen'); await page.emulateMediaType('screen');
await page.setContent(template); await page.setContent(template);
const element = await page.$('#pageFooter'); const element = await page.$('#pageFooter');

3290
print/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff