diff --git a/client/helpers/crudModelHelper.js b/client/helpers/crudModelHelper.js index a9e29e882..8bd33a955 100644 --- a/client/helpers/crudModelHelper.js +++ b/client/helpers/crudModelHelper.js @@ -18,5 +18,7 @@ module.exports.crudModel = { accept(); }); }, - refresh: () => {} + refresh: () => {}, + addFilter: () => {}, + applyFilter: () => {} }; diff --git a/client/order/src/catalog/index.html b/client/order/src/catalog/index.html index 5485c41a8..ec21fb2ea 100644 --- a/client/order/src/catalog/index.html +++ b/client/order/src/catalog/index.html @@ -1,9 +1,9 @@ - + data="items" on-data-change="$ctrl.onDataChange()" > @@ -11,17 +11,28 @@ - {{model.data.length}} results + {{model.data.length || 0}} results - - + + + + + + @@ -78,14 +89,14 @@ - + No results - diff --git a/client/order/src/catalog/index.js b/client/order/src/catalog/index.js index a6b6e302b..0b38a4d9e 100644 --- a/client/order/src/catalog/index.js +++ b/client/order/src/catalog/index.js @@ -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: '<', + }, }); diff --git a/client/order/src/catalog/index.spec.js b/client/order/src/catalog/index.spec.js index 7606bafe3..13f3e041b 100644 --- a/client/order/src/catalog/index.spec.js +++ b/client/order/src/catalog/index.spec.js @@ -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); }); }); }); diff --git a/client/order/src/filter/index.html b/client/order/src/filter/index.html index f94a23564..ddf2e6ffc 100644 --- a/client/order/src/filter/index.html +++ b/client/order/src/filter/index.html @@ -21,7 +21,6 @@ - { + /* $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(``)(this.$scope.$new()); - let panel = this.$panel.isolateScope().$ctrl; + this.$panel = this.$compile(``)(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: '<', + }, }); diff --git a/client/order/src/filter/index.spec.js b/client/order/src/filter/index.spec.js index 9ce70cbac..bca41d962 100644 --- a/client/order/src/filter/index.spec.js +++ b/client/order/src/filter/index.spec.js @@ -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: []}); }); }); diff --git a/client/order/src/locale/es.yml b/client/order/src/locale/es.yml index 83e8ca9c0..8e602c775 100644 --- a/client/order/src/locale/es.yml +++ b/client/order/src/locale/es.yml @@ -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 \ No newline at end of file diff --git a/services/db/install/changes/1.0-OnlyLocal/07-fieldAcl.sql b/services/db/install/changes/1.0-OnlyLocal/07-fieldAcl.sql index e17606970..052b33c34 100644 --- a/services/db/install/changes/1.0-OnlyLocal/07-fieldAcl.sql +++ b/services/db/install/changes/1.0-OnlyLocal/07-fieldAcl.sql @@ -79,4 +79,5 @@ INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission` -INSERT INTO `salix`.`fieldAcl` (`model`, `property`, `actionType`, `role`) VALUES ('TicketWeekly', '*', '*', 'employee'); \ No newline at end of file +INSERT INTO `salix`.`fieldAcl` (`model`, `property`, `actionType`, `role`) VALUES ('TicketWeekly', '*', '*', 'employee'); +INSERT INTO `salix`.`fieldAcl` (`model`, `property`, `actionType`, `role`) VALUES ('Receipt', '*', '*', 'administrative'); \ No newline at end of file diff --git a/services/loopback/common/models/tag.json b/services/loopback/common/models/tag.json index e35a7eddc..22af888da 100644 --- a/services/loopback/common/models/tag.json +++ b/services/loopback/common/models/tag.json @@ -25,6 +25,12 @@ }, "unit": { "type": "String" + }, + "isQuantitative": { + "type": "Boolean", + "mysql": { + "columnName": "isQuantitatif" + } } }, "acls": [ diff --git a/services/order/common/methods/order/catalogFilter.js b/services/order/common/methods/order/catalogFilter.js index c29b59061..80a74b78f 100644 --- a/services/order/common/methods/order/catalogFilter.js +++ b/services/order/common/methods/order/catalogFilter.js @@ -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]; }; }; diff --git a/services/order/common/methods/order/specs/catalogFilter.spec.js b/services/order/common/methods/order/specs/catalogFilter.spec.js index 508b3c5f8..d79a0433f 100644 --- a/services/order/common/methods/order/specs/catalogFilter.spec.js +++ b/services/order/common/methods/order/specs/catalogFilter.spec.js @@ -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);