Merge dev - test #1881

Merged
jsegarra merged 71 commits from dev into test 2023-12-07 08:28:25 +00:00
13 changed files with 277 additions and 100 deletions
Showing only changes of commit a4cbc33ee1 - Show all commits

View File

@ -10,5 +10,8 @@
"eslint.format.enable": true, "eslint.format.enable": true,
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint"
} },
"cSpell.words": [
"salix"
]
} }

View File

@ -0,0 +1,54 @@
module.exports = Self => {
Self.remoteMethod('getList', {
description: 'Get list of the available and active notification subscriptions',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
description: 'User to modify',
http: {source: 'path'}
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/getList`,
verb: 'GET'
}
});
Self.getList = async(id, options) => {
const activeNotificationsMap = new Map();
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const availableNotificationsMap = await Self.getAvailable(id, myOptions);
const activeNotifications = await Self.app.models.NotificationSubscription.find({
fields: ['id', 'notificationFk'],
include: {relation: 'notification'},
where: {userFk: id}
}, myOptions);
for (active of activeNotifications) {
activeNotificationsMap.set(active.notificationFk, {
id: active.id,
notificationFk: active.notificationFk,
name: active.notification().name,
description: active.notification().description,
active: true
});
availableNotificationsMap.delete(active.notificationFk);
}
return {
active: [...activeNotificationsMap.entries()],
available: [...availableNotificationsMap.entries()]
};
};
};

View File

@ -0,0 +1,13 @@
const models = require('vn-loopback/server/server').models;
describe('NotificationSubscription getList()', () => {
it('should return a list of available and active notifications of a user', async() => {
const userId = 9;
const {active, available} = await models.NotificationSubscription.getList(userId);
const notifications = await models.Notification.find({});
const totalAvailable = notifications.length - active.length;
expect(active.length).toEqual(2);
expect(available.length).toEqual(totalAvailable);
});
});

View File

@ -1,62 +1,74 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
require('../methods/notification/getList')(Self);
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
await checkModifyPermission(ctx);
});
Self.observe('before delete', async function(ctx) {
await checkModifyPermission(ctx);
});
async function checkModifyPermission(ctx) {
const models = Self.app.models; const models = Self.app.models;
const instance = ctx.instance;
const userId = ctx.options.accessToken.userId; const userId = ctx.options.accessToken.userId;
const user = await ctx.instance.userFk;
const modifiedUser = await getUserToModify(null, user, models);
if (userId != modifiedUser.id && userId != modifiedUser.bossFk) let notificationFk;
throw new UserError('You dont have permission to modify this user'); let workerId;
});
Self.remoteMethod('deleteNotification', { if (instance) {
description: 'Deletes a notification subscription', notificationFk = instance.notificationFk;
accepts: [ workerId = instance.userFk;
{ } else {
arg: 'ctx', const notificationSubscription = await models.NotificationSubscription.findById(ctx.where.id);
type: 'object', notificationFk = notificationSubscription.notificationFk;
http: {source: 'context'} workerId = notificationSubscription.userFk;
},
{
arg: 'notificationId',
type: 'number',
required: true
},
],
returns: {
type: 'object',
root: true
},
http: {
verb: 'POST',
path: '/deleteNotification'
} }
});
Self.deleteNotification = async function(ctx, notificationId) { const worker = await models.Worker.findById(workerId, {fields: ['id', 'bossFk']});
const available = await Self.getAvailable(workerId);
const hasAcl = available.has(notificationFk);
if (!hasAcl || (userId != worker.id && userId != worker.bossFk))
throw new UserError('The notification subscription of this worker cant be modified');
}
Self.getAvailable = async function(userId, options) {
const availableNotificationsMap = new Map();
const models = Self.app.models; const models = Self.app.models;
const user = ctx.req.accessToken.userId;
const modifiedUser = await getUserToModify(notificationId, null, models);
if (user != modifiedUser.id && user != modifiedUser.bossFk) const myOptions = {};
throw new UserError('You dont have permission to modify this user');
await models.NotificationSubscription.destroyById(notificationId); if (typeof options == 'object')
}; Object.assign(myOptions, options);
async function getUserToModify(notificationId, userFk, models) { const roles = await models.RoleMapping.find({
let userToModify = userFk; fields: ['roleId'],
if (notificationId) { where: {principalId: userId}
const subscription = await models.NotificationSubscription.findById(notificationId); }, myOptions);
userToModify = subscription.userFk;
} const availableNotifications = await models.NotificationAcl.find({
return await models.Worker.findOne({ fields: ['notificationFk', 'roleFk'],
fields: ['id', 'bossFk'], include: {relation: 'notification'},
where: { where: {
id: userToModify roleFk: {
inq: roles.map(role => role.roleId),
},
} }
}, myOptions);
for (available of availableNotifications) {
availableNotificationsMap.set(available.notificationFk, {
id: null,
notificationFk: available.notificationFk,
name: available.notification().name,
description: available.notification().description,
active: false
}); });
} }
return availableNotificationsMap;
};
}; };

View File

@ -1,74 +1,126 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
describe('loopback model NotificationSubscription', () => { describe('loopback model NotificationSubscription', () => {
it('Should fail to delete a notification if the user is not editing itself or a subordinate', async() => { it('should fail to add a notification subscription if the worker doesnt have ACLs', async() => {
const tx = await models.NotificationSubscription.beginTransaction({}); const tx = await models.NotificationSubscription.beginTransaction({});
try {
const options = {transaction: tx};
const user = 9;
const notificationSubscriptionId = 2;
const ctx = {req: {accessToken: {userId: user}}};
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
let error; let error;
try { try {
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options); const options = {transaction: tx, accessToken: {userId: 9}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 62}, options);
await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback();
error = e; error = e;
} }
expect(error.message).toContain('You dont have permission to modify this user'); expect(error.message).toEqual('The notification subscription of this worker cant be modified');
});
it('should fail to add a notification subscription if the user isnt editing itself or subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 1}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 9}, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; error = e;
} }
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
}); });
it('Should delete a notification if the user is editing itself', async() => { it('should fail to delete a notification subscription if the user isnt editing itself or subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({}); const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx, accessToken: {userId: 9}};
const user = 9; const notificationSubscriptionId = 2;
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
});
it('should add a notification subscription if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
await models.NotificationSubscription.create({notificationFk: 2, userFk: 9}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a notification subscription if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
const notificationSubscriptionId = 6;
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should add a notification subscription if the user is editing a subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 5}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a notification subscription if the user is editing a subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 19}};
const notificationSubscriptionId = 4; const notificationSubscriptionId = 4;
const ctx = {req: {accessToken: {userId: user}}}; await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
expect(deletedNotification).toBeNull();
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; error = e;
} }
});
it('Should delete a notification if the user is editing a subordinate', async() => { expect(error).toBeUndefined();
const tx = await models.NotificationSubscription.beginTransaction({});
try {
const options = {transaction: tx};
const user = 9;
const notificationSubscriptionId = 5;
const ctx = {req: {accessToken: {userId: user}}};
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
expect(deletedNotification).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('NotificationSubscription', 'getList', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -2788,6 +2788,11 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`) INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
VALUES VALUES
(1, 9), (1, 9),
(1, 1),
(2, 1),
(3, 9),
(4, 1),
(5, 9),
(6, 9); (6, 9);
INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`) INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`)
@ -2800,6 +2805,8 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`)
VALUES VALUES
(1, 1109), (1, 1109),
(1, 1110), (1, 1110),
(2, 1110),
(4, 1110),
(2, 1109), (2, 1109),
(1, 9), (1, 9),
(1, 3), (1, 3),

View File

@ -321,9 +321,9 @@
"Select a different client": "Seleccione un cliente distinto", "Select a different client": "Seleccione un cliente distinto",
"Fill all the fields": "Rellene todos los campos", "Fill all the fields": "Rellene todos los campos",
"The response is not a PDF": "La respuesta no es un PDF", "The response is not a PDF": "La respuesta no es un PDF",
"Ticket without Route": "Ticket sin ruta",
"Booking completed": "Reserva completada", "Booking completed": "Reserva completada",
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación", "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina", "The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina" "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina",
"The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada"
} }

View File

@ -158,7 +158,7 @@
}, },
"user": { "user": {
"type": "belongsTo", "type": "belongsTo",
"model": "Account", "model": "VnUser",
"foreignKey": "id" "foreignKey": "id"
}, },
"payMethod": { "payMethod": {

View File

@ -20,4 +20,5 @@ import './dms/create';
import './dms/edit'; import './dms/edit';
import './note/index'; import './note/index';
import './note/create'; import './note/create';
import './notifications';

View File

@ -0,0 +1,2 @@
<vn-card>
</vn-card>

View File

@ -0,0 +1,21 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $) {
super($element, $);
}
async $onInit() {
const url = await this.vnApp.getUrl(`worker/${this.$params.id}/notifications`);
window.open(url).focus();
}
}
ngModule.vnComponent('vnWorkerNotifications', {
template: require('./index.html'),
controller: Controller,
bindings: {
ticket: '<'
}
});

View File

@ -15,6 +15,7 @@
{"state": "worker.card.timeControl", "icon": "access_time"}, {"state": "worker.card.timeControl", "icon": "access_time"},
{"state": "worker.card.calendar", "icon": "icon-calendar"}, {"state": "worker.card.calendar", "icon": "icon-calendar"},
{"state": "worker.card.pda", "icon": "phone_android"}, {"state": "worker.card.pda", "icon": "phone_android"},
{"state": "worker.card.notifications", "icon": "notifications"},
{"state": "worker.card.pbx", "icon": "icon-pbx"}, {"state": "worker.card.pbx", "icon": "icon-pbx"},
{"state": "worker.card.dms.index", "icon": "cloud_upload"}, {"state": "worker.card.dms.index", "icon": "cloud_upload"},
{ {
@ -112,6 +113,14 @@
"params": { "params": {
"worker": "$ctrl.worker" "worker": "$ctrl.worker"
} }
}, {
"url": "/notifications",
"state": "worker.card.notifications",
"component": "vn-worker-notifications",
"description": "Notifications",
"params": {
"worker": "$ctrl.worker"
}
}, { }, {
"url": "/time-control?timestamp", "url": "/time-control?timestamp",
"state": "worker.card.timeControl", "state": "worker.card.timeControl",