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`)
VALUES
(1, '1899-12-30 12:15:00', 56, CURDATE(), 1, 1, 'first route', 1.8, 10, CURDATE(), CURDATE(), 1),
(2, '1899-12-30 13:20:00', 56, CURDATE(), 1, 2, 'second route', 0.2, 20, CURDATE(), CURDATE(), 9),
(3, '1899-12-30 14:30:00', 56, CURDATE(), 2, 3, 'third route', 0.5, 30, CURDATE(), CURDATE(), 10),
(4, '1899-12-30 15:45:00', 56, CURDATE(), 3, 4, 'fourth route', 0, 40, CURDATE(), CURDATE(), 12),
(5, '1899-12-30 16:00:00', 56, CURDATE(), 4, 5, 'fifth route', 0.1, 50, CURDATE(), CURDATE(), 13),
(6, NULL, 57, CURDATE(), 5, 7, 'sixth route', 1.7, 60, CURDATE(), CURDATE(), 3),
(7, NULL, 57, CURDATE(), 6, 8, 'seventh route', 0, 70, CURDATE(), CURDATE(), 5);
(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(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 9),
(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(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 12),
(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(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 3),
(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`)
VALUES
@ -2257,3 +2257,10 @@ INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
SELECT `id` FROM `vn`.`ticket`;
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]'
},
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',
buscamanButton: 'vn-route-tickets vn-button[icon="icon-buscaman"]',
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"]'
},
workerSummary: {

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
// #1528 e2e claim/detail
xdescribe('Route basic Data path', () => {
describe('Route tickets path', () => {
let browser;
let page;
@ -10,7 +9,7 @@ xdescribe('Route basic Data path', () => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('delivery', 'route');
await page.accessToSearchResult('3');
await page.accessToSearchResult('2');
await page.accessToSection('route.card.tickets');
});
@ -19,40 +18,32 @@ xdescribe('Route basic Data path', () => {
});
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');
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should confirm the buscamanButton is disabled', async() => {
const result = await page.evaluate(selector => {
return document.querySelector(selector);
}, `${selectors.routeTickets.buscamanButton} :disabled`);
expect(result).toBeTruthy();
it('should confirm the buscaman button is disabled', async() => {
await page.waitForSelector(`${selectors.routeTickets.buscamanButton}.disabled`);
});
it('should check the first ticket checkbox and confirm the buscamanButton button is no longer disabled', async() => {
await page.waitToClick(selectors.routeTickets.firstTicketCheckbox);
const result = await page.evaluate(selector => {
return document.querySelector(selector);
}, `${selectors.routeTickets.buscamanButton} :disabled`);
expect(result).toBeFalsy();
await page.waitForSelector(`${selectors.routeTickets.buscamanButton}.disabled`, {visible: false});
});
it('should check the route volume on the descriptor', async() => {
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() => {
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() => {
@ -63,23 +54,14 @@ xdescribe('Route basic Data path', () => {
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() => {
const result = await page.countElement('vn-route-tickets vn-textfield[ng-model="ticket.priority"]');
expect(result).toEqual(9);
await page.waitForNumberOfElements(selectors.routeTickets.anyTicket, 0);
});
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');
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()', () => {
const deliveryId = 56;
let originalTicket;
const routeId = 2;
const routeId = 1;
const activeCtx = {
accessToken: {userId: deliveryId},
};
@ -17,26 +17,18 @@ describe('route insertTicket()', () => {
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() => {
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);
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() => {
const ticketId = 23;
const ticketId = 2;
let error;
try {

View File

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

View File

@ -80,7 +80,7 @@
<vn-td shrink>
<vn-icon
ng-if="ticket.notes.length"
title="::{{ticket.notes[0].description}}"
title="{{ticket.notes[0].description}}"
icon="insert_drive_file"
class="bright">
</vn-icon>
@ -110,10 +110,9 @@
question="Delete ticket from route?"
on-accept="$ctrl.removeTicketFromRoute()">
</vn-confirm>
<vn-crud-model
<vn-crud-model
vn-id="possibleTicketsModel"
url="Tickets"
filter="$ctrl.possibleTicketsFilter"
url="Routes/{{$ctrl.$params.id}}/getSuggestedTickets"
data="$ctrl.possibleTickets">
</vn-crud-model>
<vn-dialog

View File

@ -9,8 +9,6 @@ class Controller extends Section {
set route(value) {
this._route = value;
if (value)
this.buildPossibleTicketsFilter();
}
get isChecked() {
@ -22,32 +20,6 @@ class Controller extends Section {
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() {
let highestPriority = Math.max(...this.$.model.data.map(tag => {
return tag.priority;
@ -134,14 +106,26 @@ class Controller extends Section {
setTicketsRoute() {
let tickets = this.getSelectedItems(this.possibleTickets);
if (tickets.length === 0) return;
for (let i = 0; i < tickets.length; i++) {
delete tickets[i].checked;
tickets[i].routeFk = this.route.id;
const updates = [];
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(() => {
this.$.model.data = this.$.model.data.concat(tickets);
});
const data = {creates: [], updates: updates, deletes: []};
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) {

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()', () => {
it('should return the highest value found in priorities plus 1', () => {
controller.$.model = {data: [
@ -228,13 +192,13 @@ describe('Route', () => {
});
describe('setTicketsRoute()', () => {
it('should perform a POST query to add tickets to the route', done => {
controller.$.possibleTicketsModel = {save: () => {}};
jest.spyOn(controller.$.possibleTicketsModel, 'save').mockReturnValue(Promise.resolve());
it('should perform a POST query to add tickets to the route', () => {
controller.$.model = {data: [
{id: 1, checked: false}
]};
const existingTicket = controller.$.model.data[0];
controller.route = {id: 111};
controller.possibleTickets = [
@ -245,15 +209,16 @@ describe('Route', () => {
];
let expectedResult = [
{checked: false, id: 1},
{id: 3, routeFk: 111},
{id: 5, routeFk: 111}
existingTicket,
{id: 3},
{id: 5}
];
controller.setTicketsRoute().then(() => {
expect(controller.$.model.data).toEqual(expectedResult);
done();
}).catch(done.fail);
$httpBackend.expectPOST(`Tickets/crud`).respond();
controller.setTicketsRoute();
$httpBackend.flush();
expect(controller.$.model.data).toEqual(expectedResult);
});
});

View File

@ -11,6 +11,9 @@
"Zone": {
"dataSource": "vn"
},
"ZoneAgencyMode": {
"dataSource": "vn"
},
"ZoneClosure": {
"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"
}
]
}