catalog order by tag #773

This commit is contained in:
Joan Sanchez 2018-10-31 14:56:54 +01:00
parent 7398d20132
commit bb50166e02
12 changed files with 331 additions and 171 deletions

View File

@ -18,5 +18,7 @@ module.exports.crudModel = {
accept();
});
},
refresh: () => {}
refresh: () => {},
addFilter: () => {},
applyFilter: () => {}
};

View File

@ -1,9 +1,9 @@
<vn-crud-model
<vn-crud-model auto-load="false"
vn-id="model"
url="/order/api/Orders/CatalogFilter"
filter="$ctrl.filter"
limit="50"
data="items" auto-load="false">
data="items" on-data-change="$ctrl.onDataChange()" >
</vn-crud-model>
<vn-horizontal>
@ -11,17 +11,28 @@
<vn-card>
<vn-vertical>
<vn-horizontal class="catalog-header" pad-medium-h>
<vn-one>{{model.data.length}} <span translate>results</span></vn-one>
<vn-one>{{model.data.length || 0}} <span translate>results</span></vn-one>
<vn-one>
<vn-autocomplete vn-none
data="$ctrl.orderList"
initial-data="$ctrl.orderBy"
field="$ctrl.orderBy"
on-change="$ctrl.setOrder(value)"
show-field="name"
value-field="order"
label="Order by">
</vn-autocomplete>
<vn-horizontal>
<vn-autocomplete vn-id="field" vn-one
data="$ctrl.fieldList"
initial-data="$ctrl.field"
field="$ctrl.field"
translate-fields="['name']"
show-field="name"
value-field="field"
label="Order by">
</vn-autocomplete>
<vn-autocomplete vn-one
data="$ctrl.wayList"
initial-data="$ctrl.way"
field="$ctrl.way"
translate-fields="['name']"
show-field="name"
value-field="way"
label="Order">
</vn-autocomplete>
</vn-horizontal>
</vn-one>
</vn-horizontal>
<vn-horizontal class="catalog-list" pad-small>
@ -78,14 +89,14 @@
</vn-one>
</section>
</vn-horizontal>
<vn-horizontal ng-if="model.data.length == 0">
<vn-horizontal ng-if="!model.data || model.data.length == 0">
<vn-one pad-small translate style="text-align: center">
No results
</vn-one>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-pagination
<vn-pagination margin-small-v
model="model"
scroll-selector="ui-view">
</vn-pagination>

View File

@ -2,48 +2,104 @@ import ngModule from '../module';
import './style.scss';
class Controller {
constructor($scope, $state, $translate) {
constructor($scope, $state) {
this.$scope = $scope;
this.$state = $state;
this.orderList = [
{
order: 'relevancy DESC, name',
name: $translate.instant('Default order')
},
{
order: 'name',
name: $translate.instant('Ascendant name')
},
{
order: 'name DESC',
name: $translate.instant('Descendant name')
},
{
order: 'price',
name: $translate.instant('Ascendant price')
},
{
order: 'price DESC',
name: $translate.instant('Descendant price')
}
// Static autocomplete data
this.wayList = [
{way: 'ASC', name: 'Ascendant'},
{way: 'DESC', name: 'Descendant'},
];
this.orderBy = this.orderList[0].order;
this.filter = {
order: this.orderBy
this.defaultFieldList = [
{field: 'relevancy DESC, name', name: 'Name'},
{field: 'price', name: 'Price'}
];
this.fieldList = [];
this.fieldList = this.fieldList.concat(this.defaultFieldList);
this._way = this.wayList[0].way;
this._field = this.fieldList[0].field;
}
/**
* Fills order autocomplete with tags
* obtained from last filtered
*/
onDataChange() {
const items = this.$scope.model.data;
if (!items) return;
this.fieldList = [];
this.fieldList = this.fieldList.concat(this.defaultFieldList);
items.forEach(item => {
item.tags.forEach(itemTag => {
const alreadyAdded = this.fieldList.find(order => {
return order.field == itemTag.tagFk;
});
if (!alreadyAdded)
this.fieldList.push({
name: itemTag.name,
field: itemTag.tagFk,
isTag: true
});
});
});
}
/**
* Get order way ASC/DESC
*/
get way() {
return this._way;
}
set way(value) {
this._way = value;
if (value)
this.applyOrder();
}
/**
* Get order fields
*/
get field() {
return this._field;
}
set field(value) {
this._field = value;
if (value)
this.applyOrder();
}
/**
* Returns order param
*
* @return {Object} - Order param
*/
getOrderBy() {
let field = this.$scope.field;
let args = {
field: this.field,
way: this.way
};
if (field.selection && field.selection.isTag)
args.isTag = true;
return args;
}
get orderBy() {
return this._orderBy;
}
set orderBy(value) {
this._orderBy = value;
}
setOrder(order) {
this.$scope.model.filter.order = order;
this.$scope.model.refresh();
/**
* Apply order to model
*/
applyOrder() {
this.$scope.model.addFilter(null, {orderBy: this.getOrderBy()});
}
preview(event, item) {
@ -56,18 +112,17 @@ class Controller {
}
$onChanges() {
if (this.order && this.order.isConfirmed) {
if (this.order && this.order.isConfirmed)
this.$state.go('order.card.line');
}
}
}
Controller.$inject = ['$scope', '$state', '$translate'];
Controller.$inject = ['$scope', '$state'];
ngModule.component('vnOrderCatalog', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
}
order: '<',
},
});

View File

@ -15,16 +15,49 @@ describe('Order', () => {
$componentController = _$componentController_;
$scope = $rootScope.$new();
$scope.model = crudModel;
$scope.field = {};
controller = $componentController('vnOrderCatalog', {$scope: $scope});
}));
describe('setOrder()', () => {
it(`should apply filter order and call model refresh() method`, () => {
spyOn(controller.$scope.model, 'refresh');
controller.setOrder('relevancy DESC');
describe('onDataChange()', () => {
it(`should return an object with order params`, () => {
let expectedList = [
{field: 'relevancy DESC, name', name: 'Name'},
{field: 'price', name: 'Price'},
{field: 4, name: 'Length', isTag: true}
];
$scope.model.data = [{id: 1, name: 'My Item', tags: [
{tagFk: 4, name: 'Length'}
]}];
expect(controller.$scope.model.filter.order).toEqual('relevancy DESC');
expect(controller.$scope.model.refresh).toHaveBeenCalledWith();
controller.onDataChange();
expect(controller.fieldList).toEqual(expectedList);
});
});
describe('getOrderBy()', () => {
it(`should return an object with order params`, () => {
controller.field = 'relevancy DESC, name';
controller.way = 'DESC';
let expectedResult = {field: 'relevancy DESC, name', way: 'DESC'};
let result = controller.getOrderBy();
expect(result).toEqual(expectedResult);
});
});
describe('applyOrder()', () => {
it(`should apply order param to model calling getOrderBy()`, () => {
controller.field = 'relevancy DESC, name';
controller.way = 'ASC';
let expectedOrder = {orderBy: controller.getOrderBy()};
spyOn(controller, 'getOrderBy').and.callThrough();
spyOn(controller.$scope.model, 'addFilter');
controller.applyOrder();
expect(controller.getOrderBy).toHaveBeenCalledWith();
expect(controller.$scope.model.addFilter).toHaveBeenCalledWith(null, expectedOrder);
});
});
});

View File

@ -21,7 +21,6 @@
</vn-icon>
</vn-one>
</vn-horizontal>
<vn-horizontal pad-medium class="catalog-header">
<vn-autocomplete vn-one
vn-id="type"

View File

@ -12,7 +12,7 @@ class Controller {
this.itemTypes = [];
this.tags = [];
/* $transitions.onSuccess({}, transition => {
/* $transitions.onSuccess({}, transition => {
let params = {};
if (this.category)
params.category = this.category;
@ -42,12 +42,15 @@ class Controller {
if (this.$stateParams.category)
category = JSON.parse(this.$stateParams.category);
if (this.$stateParams.type)
type = JSON.parse(this.$stateParams.type);
if (category && category.id)
this.category = category;
if (type && type.id)
this.type = type;
});
@ -71,7 +74,7 @@ class Controller {
this._category = value;
this.updateStateParams();
let query = `/item/api/ItemCategories/${value.id}/itemTypes`;
const query = `/item/api/ItemCategories/${value.id}/itemTypes`;
this.$http.get(query).then(res => {
this.itemTypes = res.data;
});
@ -99,42 +102,47 @@ class Controller {
onSearch(event) {
if (event.key !== 'Enter') return;
this.tags.push({
value: this.value
value: this.value,
});
this.$scope.search.value = null;
this.applyFilters();
}
applyFilters() {
let newArgs = {orderFk: this.order.id};
let model = this.catalog.$scope.model;
if (this.category)
newArgs.categoryFk = this.category.id;
if (this.type)
newArgs.typeFk = this.type.id;
model.params = {
args: newArgs,
tags: this.tags
};
this.catalog.$scope.model.refresh();
}
remove(index) {
this.tags.splice(index, 1);
this.applyFilters();
if (this.tags.length > 0)
this.applyFilters();
}
applyFilters() {
let newParams = {};
const newFilter = {};
const model = this.catalog.$scope.model;
if (this.category)
newFilter.categoryFk = this.category.id;
if (this.type)
newFilter.typeFk = this.type.id;
newParams = {
orderFk: this.order.id,
orderBy: this.catalog.getOrderBy(),
tags: this.tags,
};
model.applyFilter({where: newFilter}, newParams);
}
openPanel(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.$panel = this.$compile(`<vn-order-search-panel/>`)(this.$scope.$new());
let panel = this.$panel.isolateScope().$ctrl;
this.$panel = this.$compile(`<vn-order-catalog-search-panel/>`)(this.$scope.$new());
const panel = this.$panel.isolateScope().$ctrl;
panel.filter = this.filter;
panel.onSubmit = filter => this.onPanelSubmit(filter);
@ -156,16 +164,18 @@ class Controller {
}
updateStateParams() {
let params = {};
const params = {};
if (this.category)
params.category = JSON.stringify(this.category);
if (this.type)
params.type = JSON.stringify(this.type);
else
params.type = undefined;
this.$state.go(this.$state.current.name, params);
}
}
@ -176,9 +186,9 @@ ngModule.component('vnCatalogFilter', {
template: require('./index.html'),
controller: Controller,
require: {
catalog: '^vnOrderCatalog'
catalog: '^vnOrderCatalog',
},
bindings: {
order: '<'
}
order: '<',
},
});

View File

@ -26,7 +26,10 @@ describe('Order', () => {
$state.current.name = 'my.current.state';
controller = $componentController('vnCatalogFilter', {$scope: $scope, $state});
controller.catalog = {
$scope: $scope
$scope: $scope,
getOrderBy: () => {
return {field: 'relevancy DESC, name', way: 'DESC'};
}
};
}));
@ -101,15 +104,14 @@ describe('Order', () => {
describe('applyFilters()', () => {
it(`should set type property to null, call updateStateParams() method and not call applyFilters()`, () => {
spyOn(controller.catalog.$scope.model, 'refresh');
spyOn(controller.catalog.$scope.model, 'applyFilter');
controller.order = {id: 4};
$scope.$digest();
controller.applyFilters();
let result = {args: {orderFk: 4, categoryFk: 1, typeFk: 1}, tags: []};
expect(controller.catalog.$scope.model.params).toEqual(result);
expect(controller.catalog.$scope.model.refresh).toHaveBeenCalledWith();
expect(controller.catalog.$scope.model.applyFilter).toHaveBeenCalledWith(
{where: {categoryFk: 1, typeFk: 1}},
{orderFk: 4, orderBy: controller.catalog.getOrderBy(), tags: []});
});
});

View File

@ -11,9 +11,8 @@ Accessories: Complemento
Category: Reino
Search tag: Buscar etiqueta
Order by: Ordenar por
Default order: Orden predeterminado
Ascendant name: Nombre ascendiente
Descendant name: Nombre descendiente
Ascendant price: Precio ascendiente
Descendant price: Precio descendiente
Order: Orden
Price: Precio
Ascendant: Ascendente
Descendant: Descendente
Created from: Creado desde

View File

@ -80,3 +80,4 @@ INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`
INSERT INTO `salix`.`fieldAcl` (`model`, `property`, `actionType`, `role`) VALUES ('TicketWeekly', '*', '*', 'employee');
INSERT INTO `salix`.`fieldAcl` (`model`, `property`, `actionType`, `role`) VALUES ('Receipt', '*', '*', 'administrative');

View File

@ -25,6 +25,12 @@
},
"unit": {
"type": "String"
},
"isQuantitative": {
"type": "Boolean",
"mysql": {
"columnName": "isQuantitatif"
}
}
},
"acls": [

View File

@ -6,89 +6,87 @@ module.exports = Self => {
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'Object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
arg: 'orderFk',
type: 'Number',
required: true
},
{
arg: 'args',
arg: 'orderBy',
type: 'Object',
description: 'orderFk, categoryFk, typeFk',
required: true,
http: {source: 'query'}
description: 'Items order',
required: true
},
{
arg: 'filter',
type: 'Object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
},
{
arg: 'tags',
type: ['Object'],
description: 'Request tags',
http: {source: 'query'}
}
description: 'Filter by tag'
},
],
returns: {
type: ['Object'],
root: true
root: true,
},
http: {
path: `/catalogFilter`,
verb: 'GET'
}
verb: 'GET',
},
});
Self.catalogFilter = async (filter, args, tags) => {
let stmts = [];
Self.catalogFilter = async (orderFk, orderBy, filter, tags) => {
let conn = Self.dataSource.connector;
const stmts = [];
let stmt;
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.item');
stmt = new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.item
(PRIMARY KEY (itemFk)) ENGINE = MEMORY
SELECT DISTINCT
(PRIMARY KEY (itemFk)) ENGINE = MEMORY
SELECT DISTINCT
i.id AS itemFk,
i.typeFk,
it.categoryFk
FROM vn.item i
FROM vn.item i
JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.itemCategory ic ON ic.id = it.categoryFk`
);
JOIN vn.itemCategory ic ON ic.id = it.categoryFk`);
// Filter by tag
if (tags) {
let i = 1;
for (let tag of tags) {
let tAlias = `it${i++}`;
for (const tag of tags) {
const tAlias = `it${i++}`;
if (tag.tagFk) {
if (tag.tagFk)
stmt.merge({
sql: `JOIN vn.itemTag ${tAlias} ON ${tAlias}.itemFk = i.id
AND ${tAlias}.tagFk = ?
AND ${tAlias}.value LIKE ?`,
params: [tag.tagFk, `%${tag.value}%`]
params: [tag.tagFk, `%${tag.value}%`],
});
} else {
else
stmt.merge({
sql: `JOIN vn.itemTag ${tAlias} ON ${tAlias}.itemFk = i.id
AND ${tAlias}.value LIKE ?`,
params: [`%${tag.value}%`]
params: [`%${tag.value}%`],
});
}
}
}
if (args.typeFk)
stmt.merge({
sql: 'WHERE it.categoryFk = ? AND i.typeFk = ?',
params: [args.categoryFk, args.typeFk]
});
stmt.merge(conn.makeWhere(filter.where));
stmts.push(stmt);
let order = await Self.findById(args.orderFk);
// Calculate items
const order = await Self.findById(orderFk);
stmts.push(new ParameterizedSQL(
'CALL vn.ticketCalculate(?, ?, ?)', [
order.landed,
order.address_id,
order.agency_id
order.agency_id,
]
));
@ -112,13 +110,39 @@ module.exports = Self => {
FROM tmp.ticketCalculateItem tci
JOIN vn.item i ON i.id = tci.itemFk
JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.worker w on w.id = it.workerFk`
);
stmt.merge(Self.buildSuffix(filter));
// stmt.merge(Self.buildOrderBy(orderBy));
let itemsIndex = stmts.push(stmt) - 1;
JOIN vn.worker w on w.id = it.workerFk`);
let pricesIndex = stmts.push(
// Apply order by tag
if (orderBy.isTag) {
stmt.merge({
sql: `
LEFT JOIN vn.itemTag itg
LEFT JOIN vn.tag t ON t.id = itg.tagFk
ON itg.itemFk = tci.itemFk AND itg.tagFk = ?`,
params: [orderBy.field],
});
let way = orderBy.way == 'DESC' ? 'DESC' : 'ASC';
let tag = await Self.app.models.Tag.findById(orderBy.field);
let orderSql = `
ORDER BY
itg.value IS NULL,
${tag.isQuantitative ? 'CAST(itg.value AS SIGNED)' : 'itg.value'}
${way}`;
stmt.merge(orderSql);
} else {
// Apply order by field
let orderParam = `${orderBy.field} ${orderBy.way}`;
orderParam = orderParam.split(/\s*,/).map(param => param.trim());
stmt.merge(conn.makeOrderBy(orderParam));
}
stmt.merge(Self.makeLimit(filter));
const itemsIndex = stmts.push(stmt) - 1;
// Apply item prices
const pricesIndex = stmts.push(
`SELECT
tcp.itemFk,
tcp.grouping,
@ -127,32 +151,51 @@ module.exports = Self => {
tcp.warehouseFk,
w.name AS warehouse
FROM tmp.ticketComponentPrice tcp
JOIN vn.warehouse w ON w.id = tcp.warehouseFk`
) - 1;
JOIN vn.warehouse w ON w.id = tcp.warehouseFk`) - 1;
// Get tags from all items
const itemTagsIndex = stmts.push(
`SELECT
it.tagFk,
it.itemFk,
t.name
FROM tmp.ticketCalculateItem tci
JOIN vn.itemTag it ON it.itemFk = tci.itemFk
JOIN vn.tag t ON t.id = it.tagFk`) - 1;
// Clean temporary tables
stmts.push(
`DROP TEMPORARY TABLE
tmp.item,
tmp.ticketCalculateItem,
tmp.ticketComponentPrice`
);
tmp.ticketComponentPrice`);
let sql = ParameterizedSQL.join(stmts, ';');
let result = await Self.rawStmt(sql);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql);
// Add prices to items
result[itemsIndex].forEach(item => {
result[pricesIndex].forEach(price => {
if (item.id === price.itemFk) {
if (item.prices) {
if (item.prices)
item.prices.push(price);
} else {
else
item.prices = [price];
}
item.available = price.grouping;
}
});
});
// Attach item tags
result[itemsIndex].forEach(item => {
item.tags = [];
result[itemTagsIndex].forEach(itemTag => {
if (item.id === itemTag.itemFk)
item.tags.push(itemTag);
});
});
return result[itemsIndex];
};
};

View File

@ -1,35 +1,34 @@
const app = require(`${servicesDir}/order/server/server`);
describe('order catalogFilter()', () => {
it('should return an array of items', async() => {
it('should return an array of items', async () => {
let filter = {
order: 'relevancy DESC, name'
where: {
categoryFk: 1,
typeFk: 2
}
};
let args = {
orderFk: 11,
categoryFk: 1,
typeFk: 2
};
let tags = [];
let result = await app.models.Order.catalogFilter(filter, args, tags);
let orderFk = 11;
let orderBy = {field: 'relevancy DESC, name', way: 'DESC'};
let result = await app.models.Order.catalogFilter(orderFk, orderBy, filter, tags);
let firstItemId = result[0].id;
expect(result.length).toEqual(2);
expect(firstItemId).toEqual(1);
});
it('should return an array of items based on tag filter', async() => {
it('should return an array of items based on tag filter', async () => {
let filter = {
order: 'relevancy DESC, name'
};
let args = {
orderFk: 11,
categoryFk: 1,
typeFk: 2
where: {
categoryFk: 1,
typeFk: 2
}
};
let tags = [{tagFk: 56, value: 'Object2'}];
let result = await app.models.Order.catalogFilter(filter, args, tags);
let orderFk = 11;
let orderBy = {field: 'relevancy DESC, name', way: 'DESC'};
let result = await app.models.Order.catalogFilter(orderFk, orderBy, filter, tags);
let firstItemId = result[0].id;
expect(result.length).toEqual(1);