fixes #5287 Añadir campo Locker (Taquilla) en Worker/Basic Data y crear nueva sección Worker/PDA #1347

Merged
alexandre merged 6 commits from 5287-worker-locker-pda into dev 2023-02-28 12:55:36 +00:00
22 changed files with 604 additions and 1 deletions
Showing only changes of commit 94901d5b1f - Show all commits

View File

@ -1 +1,9 @@
ALTER TABLE `vn`.`worker` ADD locker INT UNSIGNED NULL UNIQUE; ALTER TABLE `vn`.`worker` ADD locker INT UNSIGNED NULL UNIQUE;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('DeviceProduction', '*', '*', 'ALLOW', 'ROLE', 'employee'),
('DeviceProductionModels', '*', '*', 'ALLOW', 'ROLE', 'employee'),
('DeviceProductionState', '*', '*', 'ALLOW', 'ROLE', 'employee'),

Pregunta permisos a Juanjo

Pregunta permisos a Juanjo
('DeviceProductionUser', '*', '*', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'deallocatePDA', '*', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'allocatePDA', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -2787,3 +2787,30 @@ INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`)
VALUES VALUES
(1, NULL, 1); (1, NULL, 1);
INSERT INTO `vn`.`deviceProductionModels` (`code`)
VALUES
('BLACKVIEW'),
('DODGEE'),
('ZEBRA');
INSERT INTO `vn`.`deviceProductionState` (`code`, `description`)
VALUES
('active', 'activo'),
('idle', 'inactivo'),
('lost', 'perdida'),
('repair', 'reparación'),
('retired', 'retirada');
INSERT INTO `vn`.`deviceProduction` (`imei`, `modelFk`, `macWifi`, `serialNumber`, `android_id`, `purchased`, `stateFk`, `isInScalefusion`, `description`)
VALUES
('ime1', 'BLACKVIEW', 'macWifi1', 'serialNumber1', 'android_id1', util.VN_NOW(), 'active', 0, NULL),
('ime2', 'DODGEE', 'macWifi2', 'serialNumber2', 'android_id2', util.VN_NOW(), 'idle', 0, NULL),
('ime3', 'ZEBRA', 'macWifi3', 'serialNumber3', 'android_id3', util.VN_NOW(), 'active', 0, NULL),
('ime4', 'BLACKVIEW', 'macWifi4', 'serialNumber4', 'android_id4', util.VN_NOW(), 'idle', 0, NULL);
INSERT INTO `vn`.`deviceProductionUser` (`deviceProductionFk`, `userFk`, `created`)
VALUES
(1, 1, util.VN_NOW()),
(3, 3, util.VN_NOW());

View File

@ -1042,6 +1042,12 @@ export default {
switft: 'vn-worker-create vn-autocomplete[ng-model="$ctrl.worker.bankEntityFk"]', switft: 'vn-worker-create vn-autocomplete[ng-model="$ctrl.worker.bankEntityFk"]',
createButton: 'vn-worker-create vn-submit[label="Create"]', createButton: 'vn-worker-create vn-submit[label="Create"]',
}, },
workerPda: {
currentPDA: 'vn-worker-pda vn-textfield[ng-model="$ctrl.currentPDA.description"]',
newPDA: 'vn-worker-pda vn-autocomplete[ng-model="$ctrl.newPDA"]',
delete: 'vn-worker-pda vn-icon-button[icon=delete]',
submit: 'vn-worker-pda vn-submit[label="Assign"]',
},
invoiceOutIndex: { invoiceOutIndex: {
topbarSearch: 'vn-searchbar', topbarSearch: 'vn-searchbar',
searchResult: 'vn-invoice-out-index vn-card > vn-table > div > vn-tbody > a.vn-tr', searchResult: 'vn-invoice-out-index vn-card > vn-table > div > vn-tbody > a.vn-tr',

View File

@ -0,0 +1,41 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Worker pda path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'worker');
await page.accessToSearchResult('employeeNick');
await page.accessToSection('worker.card.pda');
});
afterAll(async() => {
await browser.close();
});
it('should check if worker has already a PDA allocated', async() => {
expect(await page.waitToGetProperty(selectors.workerPda.currentPDA, 'value')).toContain('serialNumber1');
});
it('should deallocate the PDA', async() => {
await page.waitToClick(selectors.workerPda.delete);
let message = await page.waitForSnackbar();
expect(message.text).toContain('PDA deallocated');
});
it('should allocate a new PDA', async() => {
await page.autocompleteSearch(selectors.workerPda.newPDA, 'serialNumber2');
await page.waitToClick(selectors.workerPda.submit);
let message = await page.waitForSnackbar();
expect(message.text).toContain('PDA allocated');
});
it('should check if a new PDA has been allocated', async() => {
expect(await page.waitToGetProperty(selectors.workerPda.currentPDA, 'value')).toContain('serialNumber2');
});
});

View File

@ -23,6 +23,7 @@ There is a new version, click here to reload: Hay una nueva versión, pulse aqu
Back: Volver Back: Volver
Save: Guardar Save: Guardar
Assign: Asignar
Create: Crear Create: Crear
Send: Enviar Send: Enviar
Delete: Eliminar Delete: Eliminar

View File

@ -0,0 +1,66 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('allocatePDA', {
description: 'Deallocate the worker\'s PDA',

descripcio incorrecta?

descripcio incorrecta?
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The worker id',
http: {source: 'path'}
}, {
arg: 'pda',
type: 'number',
required: true,
description: 'The pda id'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/allocatePDA`,
verb: 'POST'
}
});
Self.allocatePDA = async(ctx, options) => {
const models = Self.app.models;
const args = ctx.args;
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const pda = await models.DeviceProduction.findById(args.pda, myOptions);
if (pda.stateFk != 'idle') throw new UserError(`The PDA state is not idle`);
await pda.updateAttributes({stateFk: 'active'}, myOptions);
await models.DeviceProductionUser.create({
deviceProductionFk: args.pda,
userFk: args.id,
created: new Date()
}, myOptions);
if (tx) await tx.commit();
return {
deviceProductionFk: pda.id,
deviceProduction: {
modelFk: pda.modelFk,
serialNumber: pda.serialNumber
}
};
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,53 @@
module.exports = Self => {
Self.remoteMethodCtx('deallocatePDA', {
description: 'Deallocate the worker\'s PDA',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The worker id',
http: {source: 'path'}
}, {
arg: 'pda',
type: 'number',
required: true,
description: 'The pda id'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/deallocatePDA`,
verb: 'POST'
}
});
Self.deallocatePDA = async(ctx, options) => {
const models = Self.app.models;
const args = ctx.args;
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const pda = await models.DeviceProduction.findById(args.pda, myOptions);
await pda.updateAttributes({stateFk: 'idle'}, myOptions);
await models.DeviceProductionUser.destroyAll({userFk: args.id, deviceProductionFk: args.pda}, myOptions);
if (tx) await tx.commit();
return pda;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,46 @@
const models = require('vn-loopback/server/server').models;
describe('Worker allocatePDA()', () => {
it('should allocate a new worker\'s PDA', async() => {
const tx = await models.Worker.beginTransaction({});
try {
const workerWithoutPDA = 1101;
const PDAWithIdleState = 4;
const options = {transaction: tx};
const ctx = {args: {id: workerWithoutPDA, pda: PDAWithIdleState}};
await models.Worker.allocatePDA(ctx, options);
const deviceProduction = await models.DeviceProduction.findById(PDAWithIdleState, null, options);
const deviceProductionUser = await models.DeviceProductionUser
.findById(PDAWithIdleState, null, options);
expect(deviceProduction.stateFk).toEqual('active');
expect(deviceProductionUser).not.toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw error trying to allocate a non idle PDA', async() => {
const tx = await models.Worker.beginTransaction({});
let error;
try {
const workerWithoutPDA = 1101;
const PDAWithActiveState = 1;
const options = {transaction: tx};
const ctx = {args: {id: workerWithoutPDA, pda: PDAWithActiveState}};
await models.Worker.allocatePDA(ctx, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,26 @@
const models = require('vn-loopback/server/server').models;
describe('Worker deallocatePDA()', () => {
it('should deallocate a worker\'s PDA', async() => {
const tx = await models.Worker.beginTransaction({});
try {
const workerWithPDA = 1;
const PDAWithActiveState = 1;
const options = {transaction: tx};
const ctx = {args: {id: workerWithPDA, pda: PDAWithActiveState}};
await models.Worker.deallocatePDA(ctx, options);
const deviceProduction = await models.DeviceProduction.findById(PDAWithActiveState, null, options);
const deviceProductionUser = await models.DeviceProductionUser
.findById(PDAWithActiveState, null, options);
expect(deviceProduction.stateFk).toEqual('idle');
expect(deviceProductionUser).toBe(null);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -20,6 +20,18 @@
"Device": { "Device": {
"dataSource": "vn" "dataSource": "vn"
}, },
"DeviceProduction": {
"dataSource": "vn"
},
"DeviceProductionModels": {
"dataSource": "vn"
},
"DeviceProductionState": {
"dataSource": "vn"
},
"DeviceProductionUser": {
"dataSource": "vn"
},
"EducationLevel": { "EducationLevel": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,15 @@
{
"name": "DeviceProductionModels",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProductionModels"
}
},
"properties": {
"code": {
"type": "string",
"id": true
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "DeviceProductionState",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProductionState"
}
},
"properties": {
"code": {
"type": "string",
"id": true
},
"description": {
"type": "string"
}
}
}

View File

@ -0,0 +1,33 @@
{
"name": "DeviceProductionUser",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProductionUser"
}
},
"properties": {
"deviceProductionFk": {
"type": "number",
"id": true
},
"userFk": {
"type": "number"
},
"created": {
"type": "date"
}
},
"relations": {
"deviceProduction": {
"type": "belongsTo",
"model": "DeviceProduction",
"foreignKey": "deviceProductionFk"
},
"user": {
"type": "belongsTo",
"model": "User",
"foreignKey": "userFk"
}
}
}

View File

@ -0,0 +1,54 @@
{
"name": "DeviceProduction",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProduction"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"imei": {
"type": "string"
},
"modelFk": {
"type": "number"
},
"macWifi": {
"type" : "string"
},
"serialNumber": {
"type" : "string"
},
"android_id": {
"type" : "string"
},
"purchased": {
"type" : "date"
},
"stateFk": {
"type" : "string"
},
"isInScaleFusion": {
"type" : "boolean"
},
"description": {
"type" : "string"
}
},
"relations": {
"model": {
"type": "belongsTo",
"model": "DeviceProductionModels",
"foreignKey": "modelFk"
},
"state": {
"type": "belongsTo",
"model": "DeviceProductionState",
"foreignKey": "stateFk"
}
}
}

View File

@ -14,6 +14,8 @@ module.exports = Self => {
require('../methods/worker/holidays')(Self); require('../methods/worker/holidays')(Self);
require('../methods/worker/activeContract')(Self); require('../methods/worker/activeContract')(Self);
require('../methods/worker/new')(Self); require('../methods/worker/new')(Self);
require('../methods/worker/deallocatePDA')(Self);
require('../methods/worker/allocatePDA')(Self);
Self.validatesUniquenessOf('locker', { Self.validatesUniquenessOf('locker', {
message: 'This locker has already been assigned' message: 'This locker has already been assigned'

View File

@ -10,6 +10,7 @@ import './descriptor-popover';
import './search-panel'; import './search-panel';
import './basic-data'; import './basic-data';
import './pbx'; import './pbx';
import './pda';
import './department'; import './department';
import './calendar'; import './calendar';
import './time-control'; import './time-control';

View File

@ -23,4 +23,11 @@ worker: trabajador
Go to the worker: Ir al trabajador Go to the worker: Ir al trabajador
Click to exclude the user from getting disabled: Marcar para no deshabilitar Click to exclude the user from getting disabled: Marcar para no deshabilitar
Click to allow the user to be disabled: Marcar para deshabilitar Click to allow the user to be disabled: Marcar para deshabilitar
This user can't be disabled: Fijado para no deshabilitar This user can't be disabled: Fijado para no deshabilitar
Model: Modelo
Serial Number: Número de serie
Current PDA: PDA Actual
Deallocate PDA: Desasignar PDA
PDA deallocated: PDA desasignada
PDA allocated: PDA asignada
New PDA: Nueva PDA

View File

@ -0,0 +1,48 @@
<div class="vn-w-md" ng-show="$ctrl.currentPDA">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
label="Current PDA"
ng-model="$ctrl.currentPDA.description"
disabled="true">
<append>
<vn-icon-button
icon="delete"
vn-tooltip="Deallocate PDA"
ng-click="$ctrl.deallocatePDA()"
vn-acl="employee">
</vn-icon-button>
</append>
</vn-textfield>
</vn-horizontal>
</vn-card>
</div>
<form name="form" ng-show="!$ctrl.currentPDA" ng-submit="$ctrl.allocatePDA()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete
ng-model="$ctrl.newPDA"
url="DeviceProductions"
fields="['id', 'modelFk', 'serialNumber']"
where="{'stateFk': 'idle'}"
label="New PDA"
order="id"
value-field="id"
show-field="serialNumber">
<tpl-item>
<span>ID: {{id}}</span>
<span class="separator"></span>
<span>{{'Model' | translate}}: {{modelFk}}</span>
<span class="separator"></span>
<span>{{'Serial Number' | translate}}: {{serialNumber}}</span>
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!$ctrl.newPDA"
label="Assign">
</vn-submit>
</vn-button-bar>
</form>

View File

@ -0,0 +1,53 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
const filter = {
where: {userFk: this.$params.id},
include: {relation: 'deviceProduction'}
};
this.$http.get('DeviceProductionUsers', {filter}).
then(res => {
if (res.data && res.data.length > 0)
this.setCurrentPDA(res.data[0]);
});
}
deallocatePDA() {
this.$http.post(`Workers/${this.$params.id}/deallocatePDA`, {pda: this.currentPDA.deviceProductionFk})
.then(() => {
this.vnApp.showSuccess(this.$t('PDA deallocated'));
delete this.currentPDA;
});
}
allocatePDA() {
this.$http.post(`Workers/${this.$params.id}/allocatePDA`, {pda: this.newPDA})
.then(res => {
if (res.data)
this.setCurrentPDA(res.data);
this.vnApp.showSuccess(this.$t('PDA allocated'));
delete this.newPDA;
});
}
setCurrentPDA(data) {
this.currentPDA = data;
this.currentPDA.description = [];
this.currentPDA.description.push(`ID: ${this.currentPDA.deviceProductionFk}`);
this.currentPDA.description.push(`${this.$t('Model')}: ${this.currentPDA.deviceProduction.modelFk}`);
this.currentPDA.description.push(`${this.$t('Serial Number')}: ${this.currentPDA.deviceProduction.serialNumber}`);
this.currentPDA.description = this.currentPDA.description.join(' ');
}
}
Controller.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnWorkerPda', {
template: require('./index.html'),
controller: Controller,
});

View File

@ -0,0 +1,72 @@
import './index';
describe('Worker', () => {
describe('Component vnWorkerPda', () => {
let $httpBackend;
let $scope;
let $element;
let controller;
beforeEach(ngModule('worker'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$element = angular.element('<vn-worker-pda></vn-worker-pda>');
controller = $componentController('vnWorkerPda', {$element, $scope});
$httpBackend.expectGET(`DeviceProductionUsers`).respond();
}));
describe('deallocatePDA()', () => {
it('should make an HTTP Post query to deallocatePDA', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.currentPDA = {deviceProductionFk: 1};
controller.$params.id = 1;
$httpBackend
.expectPOST(`Workers/${controller.$params.id}/deallocatePDA`,
{pda: controller.currentPDA.deviceProductionFk})
.respond();
controller.deallocatePDA();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.currentPDA).toBeUndefined();
});
});
describe('allocatePDA()', () => {
it('should make an HTTP Post query to allocatePDA', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.newPDA = 4;
controller.$params.id = 1;
$httpBackend
.expectPOST(`Workers/${controller.$params.id}/allocatePDA`,
{pda: controller.newPDA})
.respond();
controller.allocatePDA();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.newPDA).toBeUndefined();
});
});
describe('setCurrentPDA()', () => {
it('should set CurrentPDA', () => {
const data = {
deviceProductionFk: 1,
deviceProduction: {
modelFk: 1,
serialNumber: 1
}
};
controller.setCurrentPDA(data);
expect(controller.currentPDA).toBeDefined();
expect(controller.currentPDA.description).toBeDefined();
});
});
});
});

View File

@ -0,0 +1,6 @@
span.separator{
border-left: 1px solid black;
height: 100%;
margin: 0 10px;
}

View File

@ -15,6 +15,7 @@
{"state": "worker.card.calendar", "icon": "icon-calendar"}, {"state": "worker.card.calendar", "icon": "icon-calendar"},
{"state": "worker.card.timeControl", "icon": "access_time"}, {"state": "worker.card.timeControl", "icon": "access_time"},
{"state": "worker.card.dms.index", "icon": "cloud_upload"}, {"state": "worker.card.dms.index", "icon": "cloud_upload"},
{"state": "worker.card.pda", "icon": "contact_support"},
{ {
"icon": "icon-wiki", "icon": "icon-wiki",
"external":true, "external":true,
@ -141,6 +142,13 @@
"component": "vn-worker-create", "component": "vn-worker-create",
"description": "New worker", "description": "New worker",
"acl": ["hr"] "acl": ["hr"]
},
{
"url": "/pda",
"state": "worker.card.pda",
"component": "vn-worker-pda",
"description": "PDA",
"acl": ["employee"]
} }
] ]
} }