Merge pull request '2705-route_tickets_refactor' (#576) from 2705-route_tickets_refactor into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #576
Reviewed-by: Joan Sanchez <joan@verdnatura.es>
This commit is contained in:
Carlos Jimenez Ruiz 2021-03-24 08:04:34 +00:00
commit 7c46f513e2
12 changed files with 203 additions and 138 deletions

View File

@ -564,13 +564,13 @@ INSERT INTO `vn`.`zoneConfig` (`scope`) VALUES ('1');
INSERT INTO `vn`.`route`(`id`, `time`, `workerFk`, `created`, `vehicleFk`, `agencyModeFk`, `description`, `m3`, `cost`, `started`, `finished`, `zoneFk`) INSERT INTO `vn`.`route`(`id`, `time`, `workerFk`, `created`, `vehicleFk`, `agencyModeFk`, `description`, `m3`, `cost`, `started`, `finished`, `zoneFk`)
VALUES VALUES
(1, '1899-12-30 12:15:00', 56, CURDATE(), 1, 1, 'first route', 1.8, 10, CURDATE(), CURDATE(), 1), (1, '1899-12-30 12:15:00', 56, CURDATE(), 1, 1, 'first route', 1.8, 10, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 1),
(2, '1899-12-30 13:20:00', 56, CURDATE(), 1, 2, 'second route', 0.2, 20, CURDATE(), CURDATE(), 9), (2, '1899-12-30 13:20:00', 56, CURDATE(), 1, 2, 'second route', 0.2, 20, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 9),
(3, '1899-12-30 14:30:00', 56, CURDATE(), 2, 3, 'third route', 0.5, 30, CURDATE(), CURDATE(), 10), (3, '1899-12-30 14:30:00', 56, CURDATE(), 2, 3, 'third route', 0.5, 30, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 10),
(4, '1899-12-30 15:45:00', 56, CURDATE(), 3, 4, 'fourth route', 0, 40, CURDATE(), CURDATE(), 12), (4, '1899-12-30 15:45:00', 56, CURDATE(), 3, 4, 'fourth route', 0, 40, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 12),
(5, '1899-12-30 16:00:00', 56, CURDATE(), 4, 5, 'fifth route', 0.1, 50, CURDATE(), CURDATE(), 13), (5, '1899-12-30 16:00:00', 56, CURDATE(), 4, 5, 'fifth route', 0.1, 50, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 13),
(6, NULL, 57, CURDATE(), 5, 7, 'sixth route', 1.7, 60, CURDATE(), CURDATE(), 3), (6, NULL, 57, CURDATE(), 5, 7, 'sixth route', 1.7, 60, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 3),
(7, NULL, 57, CURDATE(), 6, 8, 'seventh route', 0, 70, CURDATE(), CURDATE(), 5); (7, NULL, 57, CURDATE(), 6, 8, 'seventh route', 0, 70, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 5);
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`) INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`)
VALUES VALUES
@ -2257,3 +2257,10 @@ INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
SELECT `id` FROM `vn`.`ticket`; SELECT `id` FROM `vn`.`ticket`;
CALL `vn`.`ticket_doRecalc`(); CALL `vn`.`ticket_doRecalc`();
INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
VALUES
(1, 1, 1),
(2, 1, 2),
(3, 6, 5),
(4, 7, 1);

View File

@ -790,10 +790,11 @@ export default {
saveButton: 'vn-route-basic-data button[type=submit]' saveButton: 'vn-route-basic-data button[type=submit]'
}, },
routeTickets: { routeTickets: {
firstTicketPriority: 'vn-route-tickets vn-tr:nth-child(1) vn-textfield[ng-model="ticket.priority"]', firstTicketPriority: 'vn-route-tickets vn-tr:nth-child(1) vn-input-number[ng-model="ticket.priority"]',
firstTicketCheckbox: 'vn-route-tickets vn-tr:nth-child(1) vn-check', firstTicketCheckbox: 'vn-route-tickets vn-tr:nth-child(1) vn-check',
buscamanButton: 'vn-route-tickets vn-button[icon="icon-buscaman"]', buscamanButton: 'vn-route-tickets vn-button[icon="icon-buscaman"]',
firstTicketDeleteButton: 'vn-route-tickets vn-tr:nth-child(1) vn-icon[icon="delete"]', firstTicketDeleteButton: 'vn-route-tickets vn-tr:nth-child(1) vn-icon[icon="delete"]',
anyTicket: 'vn-route-tickets vn-tbody > vn-tr',
confirmButton: '.vn-confirm.shown button[response="accept"]' confirmButton: '.vn-confirm.shown button[response="accept"]'
}, },
workerSummary: { workerSummary: {

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js'; import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer'; import getBrowser from '../../helpers/puppeteer';
// #1528 e2e claim/detail describe('Route tickets path', () => {
xdescribe('Route basic Data path', () => {
let browser; let browser;
let page; let page;
@ -10,7 +9,7 @@ xdescribe('Route basic Data path', () => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('delivery', 'route'); await page.loginAndModule('delivery', 'route');
await page.accessToSearchResult('3'); await page.accessToSearchResult('2');
await page.accessToSection('route.card.tickets'); await page.accessToSection('route.card.tickets');
}); });
@ -19,40 +18,32 @@ xdescribe('Route basic Data path', () => {
}); });
it('should modify the first ticket priority', async() => { it('should modify the first ticket priority', async() => {
await page.write(selectors.routeTickets.firstTicketPriority, '2'); await page.clearInput(selectors.routeTickets.firstTicketPriority);
await page.type(selectors.routeTickets.firstTicketPriority, '9');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!'); expect(message.text).toContain('Data saved!');
}); });
it('should confirm the buscamanButton is disabled', async() => { it('should confirm the buscaman button is disabled', async() => {
const result = await page.evaluate(selector => { await page.waitForSelector(`${selectors.routeTickets.buscamanButton}.disabled`);
return document.querySelector(selector);
}, `${selectors.routeTickets.buscamanButton} :disabled`);
expect(result).toBeTruthy();
}); });
it('should check the first ticket checkbox and confirm the buscamanButton button is no longer disabled', async() => { it('should check the first ticket checkbox and confirm the buscamanButton button is no longer disabled', async() => {
await page.waitToClick(selectors.routeTickets.firstTicketCheckbox); await page.waitForSelector(`${selectors.routeTickets.buscamanButton}.disabled`, {visible: false});
const result = await page.evaluate(selector => {
return document.querySelector(selector);
}, `${selectors.routeTickets.buscamanButton} :disabled`);
expect(result).toBeFalsy();
}); });
it('should check the route volume on the descriptor', async() => { it('should check the route volume on the descriptor', async() => {
const result = await page.waitToGetProperty(selectors.routeDescriptor.volume, 'innerText'); const result = await page.waitToGetProperty(selectors.routeDescriptor.volume, 'innerText');
expect(result).toEqual('1.1 / 18 m³'); expect(result).toEqual('0.2 / 50 m³');
}); });
it('should count how many tickets are in route', async() => { it('should count how many tickets are in route', async() => {
const result = await page.countElement('vn-route-tickets vn-textfield[ng-model="ticket.priority"]'); const result = await page.countElement(selectors.routeTickets.anyTicket);
expect(result).toEqual(11); expect(result).toEqual(1);
}); });
it('should delete the first ticket in route', async() => { it('should delete the first ticket in route', async() => {
@ -63,23 +54,14 @@ xdescribe('Route basic Data path', () => {
expect(message.text).toContain('Ticket removed from route'); expect(message.text).toContain('Ticket removed from route');
}); });
it('should again delete the first ticket in route', async() => {
await page.waitToClick(selectors.routeTickets.firstTicketDeleteButton);
await page.waitToClick(selectors.routeTickets.confirmButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Ticket removed from route');
});
it('should now count how many tickets are in route to find one less', async() => { it('should now count how many tickets are in route to find one less', async() => {
const result = await page.countElement('vn-route-tickets vn-textfield[ng-model="ticket.priority"]'); await page.waitForNumberOfElements(selectors.routeTickets.anyTicket, 0);
expect(result).toEqual(9);
}); });
it('should confirm the route volume on the descriptor has been updated by the changes made', async() => { // #2862 updateVolume() route descriptor no actualiza volumen
xit('should confirm the route volume on the descriptor has been updated by the changes made', async() => {
const result = await page.waitToGetProperty(selectors.routeDescriptor.volume, 'innerText'); const result = await page.waitToGetProperty(selectors.routeDescriptor.volume, 'innerText');
expect(result).toEqual('0.9 / 18 m³'); expect(result).toEqual('0 / 50 m³');
}); });
}); });

View File

@ -0,0 +1,72 @@
module.exports = Self => {
Self.remoteMethod('getSuggestedTickets', {
description: 'Returns an array of suggested tickets for the given route',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The route id',
http: {source: 'path'}
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/getSuggestedTickets`,
verb: 'GET'
}
});
Self.getSuggestedTickets = async id => {
const ticketsInRoute = await Self.app.models.Ticket.find({
where: {routeFk: id},
fields: ['id']
});
const idsToExclude = ticketsInRoute.map(ticket => ticket.id);
const route = await Self.app.models.Route.findById(id);
const zoneAgencyModes = await Self.app.models.ZoneAgencyMode.find({
where: {
agencyModeFk: route.agencyModeFk
}
});
const zoneIds = [];
for (let zoneAgencyMode of zoneAgencyModes)
zoneIds.push(zoneAgencyMode.zoneFk);
const minDate = new Date(route.finished);
minDate.setHours(0, 0, 0, 0);
const maxDate = new Date(route.finished);
maxDate.setHours(23, 59, 59, 59);
let tickets = await Self.app.models.Ticket.find({
where: {
agencyModeFk: route.agencyModeFk,
zoneFk: {inq: zoneIds},
id: {nin: idsToExclude},
landed: {between: [minDate, maxDate]}
},
include: [
{
relation: 'warehouse',
scope: {
fields: ['id', 'name']
}
},
{
relation: 'address',
scope: {
fields: ['id', 'street', 'postalCode', 'city'],
}
},
]
});
return tickets;
};
};

View File

@ -0,0 +1,30 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('route getSuggestedTickets()', () => {
it('should return an array of suggested tickets', async() => {
const activeCtx = {
accessToken: {userId: 19},
headers: {origin: 'http://localhost'}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const routeID = 1;
const ticketInRoute = await app.models.Ticket.findById(12);
await ticketInRoute.updateAttribute('routeFk', null);
const result = await app.models.Route.getSuggestedTickets(routeID);
const length = result.length;
const anyResult = result[Math.floor(Math.random() * Math.floor(length))];
expect(result.length).toEqual(1);
expect(anyResult.zoneFk).toEqual(1);
expect(anyResult.agencyModeFk).toEqual(1);
await ticketInRoute.updateAttribute('routeFk', routeID);
});
});

View File

@ -4,7 +4,7 @@ const LoopBackContext = require('loopback-context');
describe('route insertTicket()', () => { describe('route insertTicket()', () => {
const deliveryId = 56; const deliveryId = 56;
let originalTicket; let originalTicket;
const routeId = 2; const routeId = 1;
const activeCtx = { const activeCtx = {
accessToken: {userId: deliveryId}, accessToken: {userId: deliveryId},
}; };
@ -17,26 +17,18 @@ describe('route insertTicket()', () => {
done(); done();
}); });
afterAll(async done => {
try {
await originalTicket.updateAttribute('routeFk', null);
} catch (error) {
console.error(error);
}
done();
});
it('should add the ticket to a route', async() => { it('should add the ticket to a route', async() => {
originalTicket = await app.models.Ticket.findById(14); const ticketId = 12;
originalTicket = await app.models.Ticket.findById(ticketId);
await originalTicket.updateAttribute('routeFk', null);
const ticketId = 14;
const result = await app.models.Route.insertTicket(routeId, ticketId); const result = await app.models.Route.insertTicket(routeId, ticketId);
expect(result.routeFk).toEqual(2); expect(result.routeFk).toEqual(routeId);
}); });
it('should throw and error if the ticket is not suitable for the route', async() => { it('should throw and error if the ticket is not suitable for the route', async() => {
const ticketId = 23; const ticketId = 2;
let error; let error;
try { try {

View File

@ -7,6 +7,7 @@ module.exports = Self => {
require('../methods/route/getDeliveryPoint')(Self); require('../methods/route/getDeliveryPoint')(Self);
require('../methods/route/insertTicket')(Self); require('../methods/route/insertTicket')(Self);
require('../methods/route/clone')(Self); require('../methods/route/clone')(Self);
require('../methods/route/getSuggestedTickets')(Self);
Self.validate('kmStart', validateDistance, { Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000' message: 'Distance must be lesser than 1000'

View File

@ -80,7 +80,7 @@
<vn-td shrink> <vn-td shrink>
<vn-icon <vn-icon
ng-if="ticket.notes.length" ng-if="ticket.notes.length"
title="::{{ticket.notes[0].description}}" title="{{ticket.notes[0].description}}"
icon="insert_drive_file" icon="insert_drive_file"
class="bright"> class="bright">
</vn-icon> </vn-icon>
@ -112,8 +112,7 @@
</vn-confirm> </vn-confirm>
<vn-crud-model <vn-crud-model
vn-id="possibleTicketsModel" vn-id="possibleTicketsModel"
url="Tickets" url="Routes/{{$ctrl.$params.id}}/getSuggestedTickets"
filter="$ctrl.possibleTicketsFilter"
data="$ctrl.possibleTickets"> data="$ctrl.possibleTickets">
</vn-crud-model> </vn-crud-model>
<vn-dialog <vn-dialog

View File

@ -9,8 +9,6 @@ class Controller extends Section {
set route(value) { set route(value) {
this._route = value; this._route = value;
if (value)
this.buildPossibleTicketsFilter();
} }
get isChecked() { get isChecked() {
@ -22,32 +20,6 @@ class Controller extends Section {
return false; return false;
} }
buildPossibleTicketsFilter() {
let minDate = new Date(this.route.finished);
minDate.setHours(0, 0, 0, 0);
let maxDate = new Date(this.route.finished);
maxDate.setHours(23, 59, 59, 59);
this.possibleTicketsFilter = {
where: {
zoneFk: this.route.zoneFk,
routeFk: null,
landed: {between: [minDate, maxDate]},
},
include: [
{
relation: 'warehouse',
scope: {
fields: ['name']
},
}, {
relation: 'address'
}
]
};
}
getHighestPriority() { getHighestPriority() {
let highestPriority = Math.max(...this.$.model.data.map(tag => { let highestPriority = Math.max(...this.$.model.data.map(tag => {
return tag.priority; return tag.priority;
@ -134,14 +106,26 @@ class Controller extends Section {
setTicketsRoute() { setTicketsRoute() {
let tickets = this.getSelectedItems(this.possibleTickets); let tickets = this.getSelectedItems(this.possibleTickets);
if (tickets.length === 0) return; if (tickets.length === 0) return;
for (let i = 0; i < tickets.length; i++) {
delete tickets[i].checked; const updates = [];
tickets[i].routeFk = this.route.id;
for (let ticket of tickets) {
delete ticket.checked;
const update = {
where: {id: ticket.id},
data: {routeFk: this.route.id}
};
updates.push(update);
} }
return this.$.possibleTicketsModel.save().then(() => { const data = {creates: [], updates: updates, deletes: []};
this.$.model.data = this.$.model.data.concat(tickets);
}); return this.$http.post(`Tickets/crud`, data)
.then(() => {
this.$.model.data = this.$.model.data.concat(tickets);
this.vnApp.showSuccess(this.$t('Data saved!'));
});
} }
onDrop($event) { onDrop($event) {

View File

@ -37,42 +37,6 @@ describe('Route', () => {
}); });
}); });
describe('buildPossibleTicketsFilter()', () => {
it('should build the possible tickets filter', () => {
let expectedFilter = {
include: [
{
relation: 'warehouse',
scope: {
fields: ['name']
}
}, {
relation: 'address'
}
],
where: {
landed: {
between: [
jasmine.any(Date),
jasmine.any(Date)
]
},
routeFk: null,
zoneFk: 67
}
};
controller.route = {
finished: new Date(),
routeFk: null,
zoneFk: 67
};
controller.buildPossibleTicketsFilter();
expect(controller.possibleTicketsFilter).toEqual(expectedFilter);
});
});
describe('getHighestPriority()', () => { describe('getHighestPriority()', () => {
it('should return the highest value found in priorities plus 1', () => { it('should return the highest value found in priorities plus 1', () => {
controller.$.model = {data: [ controller.$.model = {data: [
@ -228,13 +192,13 @@ describe('Route', () => {
}); });
describe('setTicketsRoute()', () => { describe('setTicketsRoute()', () => {
it('should perform a POST query to add tickets to the route', done => { it('should perform a POST query to add tickets to the route', () => {
controller.$.possibleTicketsModel = {save: () => {}};
jest.spyOn(controller.$.possibleTicketsModel, 'save').mockReturnValue(Promise.resolve());
controller.$.model = {data: [ controller.$.model = {data: [
{id: 1, checked: false} {id: 1, checked: false}
]}; ]};
const existingTicket = controller.$.model.data[0];
controller.route = {id: 111}; controller.route = {id: 111};
controller.possibleTickets = [ controller.possibleTickets = [
@ -245,15 +209,16 @@ describe('Route', () => {
]; ];
let expectedResult = [ let expectedResult = [
{checked: false, id: 1}, existingTicket,
{id: 3, routeFk: 111}, {id: 3},
{id: 5, routeFk: 111} {id: 5}
]; ];
controller.setTicketsRoute().then(() => { $httpBackend.expectPOST(`Tickets/crud`).respond();
expect(controller.$.model.data).toEqual(expectedResult); controller.setTicketsRoute();
done(); $httpBackend.flush();
}).catch(done.fail);
expect(controller.$.model.data).toEqual(expectedResult);
}); });
}); });

View File

@ -11,6 +11,9 @@
"Zone": { "Zone": {
"dataSource": "vn" "dataSource": "vn"
}, },
"ZoneAgencyMode": {
"dataSource": "vn"
},
"ZoneClosure": { "ZoneClosure": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,29 @@
{
"name": "ZoneAgencyMode",
"base": "VnModel",
"options": {
"mysql": {
"table": "zoneAgencyMode"
}
},
"properties": {
"id": {
"id": true,
"type": "number"
},
"agencyModeFk": {
"type": "number"
},
"zoneFk": {
"type": "number"
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}