diff --git a/VirtoCommerce.PricingModule.Web/Content/css/pricing.css b/VirtoCommerce.PricingModule.Web/Content/css/pricing.css new file mode 100644 index 00000000..bae28c85 --- /dev/null +++ b/VirtoCommerce.PricingModule.Web/Content/css/pricing.css @@ -0,0 +1,20 @@ +.head-label { + font-size: 17px; + margin-bottom: 15px; +} + +.head-select { + width: calc(100% - 120px); +} + +button.btn.head-button { + background: #43b0e6; + color: #fff; + height: 30px; + line-height: 28px; + padding: 0 10px; +} + + button.btn.head-button:hover { + background: #43b0e6; + } \ No newline at end of file diff --git a/VirtoCommerce.PricingModule.Web/Controllers/Api/PricingModuleController.cs b/VirtoCommerce.PricingModule.Web/Controllers/Api/PricingModuleController.cs index 79647773..c4204a2b 100644 --- a/VirtoCommerce.PricingModule.Web/Controllers/Api/PricingModuleController.cs +++ b/VirtoCommerce.PricingModule.Web/Controllers/Api/PricingModuleController.cs @@ -385,6 +385,21 @@ public IHttpActionResult DeleteProductPrices(string pricelistId, [FromUri]string return StatusCode(HttpStatusCode.NoContent); } + /// + /// Delete price by ids + /// + /// + /// + [HttpDelete] + [ResponseType(typeof(void))] + [Route("api/pricing/products/prices")] + [CheckPermission(Permission = PricingPredefinedPermissions.Delete)] + public IHttpActionResult DeleteProductPrice([FromUri]string[] priceIds) + { + _pricingService.DeletePrices(priceIds); + return StatusCode(HttpStatusCode.NoContent); + } + /// /// Delete pricelists /// diff --git a/VirtoCommerce.PricingModule.Web/Localizations/en.VirtoCommerce.Pricing.json b/VirtoCommerce.PricingModule.Web/Localizations/en.VirtoCommerce.Pricing.json index b980ccb5..d58e8d72 100644 --- a/VirtoCommerce.PricingModule.Web/Localizations/en.VirtoCommerce.Pricing.json +++ b/VirtoCommerce.PricingModule.Web/Localizations/en.VirtoCommerce.Pricing.json @@ -109,7 +109,20 @@ }, "no-assignments": "Catalog does not have assigned price lists. Please assign a price list by following this link ", "manage-pricelists": "Manage pricing" - + }, + "item-prices": { + "title": "Prices for {{name}}", + "subtitle": "Edit prices", + "head-label": "Price list for new price", + "pricelist-select-placeholder": "Select a pricelist to add new price...", + "labels": { + "name": "Price List", + "currency": "Currency", + "catalog": "Catalog", + "list-price": "List price", + "sale-price": "Sale price", + "min-quantity": "Min. quantity" + } } }, "widgets": { @@ -137,6 +150,10 @@ "title": "Delete confirmation", "message": "Are you sure you want to delete ALL prices for selected Products in this Price list?" }, + "item-prices-delete": { + "title": "Delete confirmation", + "message": "Are you sure you want to delete selected Prices?" + }, "prices-save": { "title": "Save changes", "message": "The Prices has been modified. Do you want to save changes?" diff --git a/VirtoCommerce.PricingModule.Web/Scripts/blades/item/item-prices.js b/VirtoCommerce.PricingModule.Web/Scripts/blades/item/item-prices.js new file mode 100644 index 00000000..94b3c204 --- /dev/null +++ b/VirtoCommerce.PricingModule.Web/Scripts/blades/item/item-prices.js @@ -0,0 +1,225 @@ +angular.module('virtoCommerce.pricingModule') +.controller('virtoCommerce.pricingModule.itemPriceListController', ['$scope', 'platformWebApp.bladeNavigationService', 'uiGridConstants', 'virtoCommerce.pricingModule.prices', 'virtoCommerce.catalogModule.catalogs', 'platformWebApp.ui-grid.extension', 'platformWebApp.objCompareService', 'virtoCommerce.pricingModule.priceValidatorsService', 'platformWebApp.dialogService', + function ($scope, bladeNavigationService, uiGridConstants, prices, catalogs, gridOptionExtension, objCompareService, priceValidatorsService, dialogService) { + $scope.uiGridConstants = uiGridConstants; + var blade = $scope.blade; + blade.updatePermission = 'pricing:update'; + + blade.refresh = function () { + blade.isLoading = true; + prices.getProductPricelists({ id: blade.itemId }, function (pricelists) { + //Loading catalogs for assignments because they do not contains them + //Need to display name of catalog in assignments grid + catalogs.getCatalogs(function (catalogsList) { + $scope.catalogsList = catalogsList; + + blade.origEntity = []; + + //Collect all available pricelists + blade.pricelistList = _.map(pricelists, function (pricelist) { + return { + id: pricelist.id, + code: pricelist.name, + currency: pricelist.currency, + assignments: pricelist.assignments, + displayName: pricelist.name + ' - ' + pricelist.currency + }; + }); + blade.selectedPricelist = _.first(blade.pricelistList); + + var pricelistsWithPrices = _.filter(pricelists, function (pricelist) { return pricelist.prices.length > 0; }); + _.each(pricelistsWithPrices, function (pricelistWithPrices) { + var priceListData = { + name: pricelistWithPrices.name, + currency: pricelistWithPrices.currency + }; + + var catalogsId = _.pluck(pricelistWithPrices.assignments, 'catalogId'); + var catalogsName = _.map(catalogsId, function (catalogId) { + return _.findWhere($scope.catalogsList, { id: catalogId }).name; + }); + priceListData.catalog = catalogsName.join(', '); + + _.each(pricelistWithPrices.prices, function (price) { + var priceData = angular.copy(priceListData); + priceData = angular.extend(price, priceData); + blade.origEntity.push(priceData); + }); + }); + + priceValidatorsService.setAllPrices(blade.currentEntities); + blade.currentEntities = angular.copy(blade.origEntity); + blade.isLoading = false; + }); + }); + } + + $scope.createNewPricelist = function () { + var newBlade = { + id: 'pricingList', + controller: 'virtoCommerce.pricingModule.pricelistListController', + template: 'Modules/$(VirtoCommerce.Pricing)/Scripts/blades/pricelist-list.tpl.html', + title: 'pricing.blades.pricing-main.menu.pricelist-list.title', + parentRefresh: blade.refresh + }; + + bladeNavigationService.showBlade(newBlade, blade); + }; + + $scope.selectPricelist = function (entity) { + var newBlade = { + id: 'listItemChild', + currentEntityId: entity.pricelistId, + title: entity.name, + controller: 'virtoCommerce.pricingModule.pricelistDetailController', + template: 'Modules/$(VirtoCommerce.Pricing)/Scripts/blades/pricelist-detail.tpl.html' + }; + + bladeNavigationService.showBlade(newBlade, blade); + }; + + blade.onClose = function (closeCallback) { + bladeNavigationService.showConfirmationIfNeeded(isDirty(), canSave(), blade, $scope.saveChanges, closeCallback, "pricing.dialogs.prices-save.title", "pricing.dialogs.prices-save.message"); + }; + + function isDirty() { + return blade.currentEntities && !objCompareService.equal(blade.origEntity, blade.currentEntities) && blade.hasUpdatePermission() + } + + function canSave() { + return isDirty() && $scope.isValid(); + } + + $scope.isValid = function () { + return $scope.formScope && $scope.formScope.$valid && + _.all(blade.currentEntities, $scope.isListPriceValid) && + _.all(blade.currentEntities, $scope.isSalePriceValid) && + _.all(blade.currentEntities, $scope.isUniqueQtyForPricelist) && + (blade.currentEntities.length == 0 || _.some(blade.currentEntities, function (x) { return x.minQuantity == 1; })); + } + + $scope.saveChanges = function () { + blade.isLoading = true; + + angular.copy(blade.currentEntities, blade.origEntity); + if (_.any(blade.currentEntities)) { + var productPrices = { + productId: blade.itemId, + product: blade.item, + prices: blade.currentEntities + }; + prices.update({ id: blade.itemId }, productPrices, function (data) { + blade.refresh(); + }, + function (error) { bladeNavigationService.setError('Error ' + error.status, $scope.blade); }); + } + }; + + $scope.setForm = function (form) { $scope.formScope = form; } + + blade.toolbarCommands = [ + { + name: "platform.commands.save", + icon: 'fa fa-save', + executeMethod: $scope.saveChanges, + canExecuteMethod: canSave, + permission: blade.updatePermission + }, + { + name: "platform.commands.delete", + icon: 'fa fa-trash-o', + executeMethod: function () { + var selection = $scope.gridApi.selection.getSelectedRows(); + var ids = _.map(selection, function (item) { return item.id; }); + + var dialog = { + id: "confirmDeleteItem", + title: "pricing.dialogs.item-prices-delete.title", + message: "pricing.dialogs.item-prices-delete.message", + callback: function (remove) { + if (remove) { + prices.removePrice({ priceIds: ids }, function () { + //blade.refresh(); + angular.forEach(selection, function (listItem) { + blade.currentEntities.splice(blade.currentEntities.indexOf(listItem), 1); + }); + }, function (error) { + bladeNavigationService.setError('Error ' + error.status, blade); + }); + } + } + } + dialogService.showConfirmationDialog(dialog); + }, + canExecuteMethod: function () { + return $scope.gridApi && _.any($scope.gridApi.selection.getSelectedRows()); + }, + permission: 'pricing:delete' + }, + { + name: "platform.commands.refresh", + icon: 'fa fa-refresh', + executeMethod: blade.refresh, + canExecuteMethod: function () { return true; } + } + ]; + + blade.addNewPrice = function (targetPricelist) { + //populate prices data for correct work of validation service + priceValidatorsService.setAllPrices(blade.currentEntities); + + var catalogsId = _.pluck(targetPricelist.assignments, 'catalogId'); + var catalogsName = _.map(catalogsId, function (catalogId) { + return _.findWhere($scope.catalogsList, { id: catalogId }).name; + }); + + var newPrice = { + productId: blade.itemId, + list: '', + minQuantity: 1, + currency: targetPricelist.currency, + pricelistId: targetPricelist.id, + name: targetPricelist.code, + catalog: catalogsName.join(', ') + }; + blade.currentEntities.push(newPrice); + $scope.validateGridData(); + } + + $scope.isListPriceValid = priceValidatorsService.isListPriceValid; + $scope.isSalePriceValid = priceValidatorsService.isSalePriceValid; + $scope.isUniqueQtyForPricelist = priceValidatorsService.isUniqueQtyForPricelist; + + // ui-grid + $scope.setGridOptions = function (gridId, gridOptions) { + gridOptions.onRegisterApi = function (gridApi) { + $scope.gridApi = gridApi; + + gridApi.edit.on.afterCellEdit($scope, function () { + //to process validation for all rows in grid. + //e.g. if we have two rows with the same count of min qty, both of this rows will be marked as error. + //when we change data to valid in one row, another one should became valid too. + //more info about ui-grid validation: https://github.com/angular-ui/ui-grid/issues/4152 + $scope.validateGridData(); + }); + + $scope.validateGridData(); + }; + + $scope.gridOptions = gridOptions; + gridOptionExtension.tryExtendGridOptions(gridId, gridOptions); + return gridOptions; + }; + + $scope.validateGridData = function () { + if ($scope.gridApi) { + angular.forEach(blade.currentEntities, function (rowEntity) { + angular.forEach($scope.gridOptions.columnDefs, function (colDef) { + $scope.gridApi.grid.validate.runValidators(rowEntity, colDef, rowEntity[colDef.name], undefined, $scope.gridApi.grid) + }); + }); + } + }; + + blade.refresh(); +}]); diff --git a/VirtoCommerce.PricingModule.Web/Scripts/blades/item/item-prices.tpl.html b/VirtoCommerce.PricingModule.Web/Scripts/blades/item/item-prices.tpl.html new file mode 100644 index 00000000..c33a4f38 --- /dev/null +++ b/VirtoCommerce.PricingModule.Web/Scripts/blades/item/item-prices.tpl.html @@ -0,0 +1,87 @@ +
+
+ + +
+ + {{$select.selected.displayName}} + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
{{ 'platform.list.no-data' | translate }}
+
+
+
+ + + + + + + + + + + diff --git a/VirtoCommerce.PricingModule.Web/Scripts/blades/pricelist-list.js b/VirtoCommerce.PricingModule.Web/Scripts/blades/pricelist-list.js index 2f7fc1fe..dc489cad 100644 --- a/VirtoCommerce.PricingModule.Web/Scripts/blades/pricelist-list.js +++ b/VirtoCommerce.PricingModule.Web/Scripts/blades/pricelist-list.js @@ -4,7 +4,7 @@ function ($scope, pricelists, dialogService, uiGridHelper, bladeUtils) { var blade = $scope.blade; var bladeNavigationService = bladeUtils.bladeNavigationService; - blade.refresh = function () { + blade.refresh = function (parentRefresh) { blade.isLoading = true; return pricelists.search({ sort: uiGridHelper.getSortExpression($scope), @@ -14,6 +14,11 @@ function ($scope, pricelists, dialogService, uiGridHelper, bladeUtils) { blade.isLoading = false; blade.currentEntities = data.results; $scope.pageSettings.totalItems = data.totalCount; + + if (parentRefresh === true && blade.parentRefresh) { + blade.parentRefresh(); + } + return data; }).$promise; }; @@ -33,7 +38,7 @@ function ($scope, pricelists, dialogService, uiGridHelper, bladeUtils) { isNew: true, saveCallback: function (newPricelist) { newBlade.isNew = false; - blade.refresh().then(function () { + blade.refresh(true).then(function () { newBlade.currentEntityId = newPricelist.id; bladeNavigationService.showBlade(newBlade, blade); }); @@ -64,7 +69,7 @@ function ($scope, pricelists, dialogService, uiGridHelper, bladeUtils) { if (remove) { bladeNavigationService.closeChildrenBlades(blade, function () { pricelists.remove({ ids: _.pluck(list, 'id') }, - blade.refresh, + blade.refresh(true), function (error) { bladeNavigationService.setError('Error ' + error.status, blade); }); }); } diff --git a/VirtoCommerce.PricingModule.Web/Scripts/blades/prices-list.js b/VirtoCommerce.PricingModule.Web/Scripts/blades/prices-list.js index efe6b670..66cd6cc2 100644 --- a/VirtoCommerce.PricingModule.Web/Scripts/blades/prices-list.js +++ b/VirtoCommerce.PricingModule.Web/Scripts/blades/prices-list.js @@ -142,6 +142,10 @@ }, isUniqueQty: function (data) { return Math.round(data.minQuantity) > 0 && _.all(allPrices, function (x) { return x === data || Math.round(x.minQuantity) !== Math.round(data.minQuantity) }); + }, + isUniqueQtyForPricelist: function (data) { + var result = Math.round(data.minQuantity) > 0 && _.all(_.where(allPrices, { pricelistId: data.pricelistId }), function (x) { return x === data || Math.round(x.minQuantity) !== Math.round(data.minQuantity) }); + return result; } }; }]) @@ -167,4 +171,10 @@ return priceValidatorsService.isUniqueQty(rowEntity); }; }, function () { return 'Quantity value should be unique'; }); + + uiGridValidateService.setValidator('minQuantityForPricelistValidator', function () { + return function (oldValue, newValue, rowEntity, colDef) { + return priceValidatorsService.isUniqueQtyForPricelist(rowEntity); + }; + }, function () { return 'Quantity value should be unique'; }); }]); diff --git a/VirtoCommerce.PricingModule.Web/Scripts/resources/pricing.js b/VirtoCommerce.PricingModule.Web/Scripts/resources/pricing.js index 00a83986..b3102d4b 100644 --- a/VirtoCommerce.PricingModule.Web/Scripts/resources/pricing.js +++ b/VirtoCommerce.PricingModule.Web/Scripts/resources/pricing.js @@ -6,7 +6,8 @@ getProductPricesForWidget: { url: 'api/products/:id/:catalogId/pricesWidget', isArray: true }, getProductPricelists: { url: 'api/catalog/products/:id/pricelists', isArray: true }, update: { method: 'PUT' }, - remove: { method: 'DELETE', url: 'api/pricing/pricelists/:priceListId/products/prices' } + remove: { method: 'DELETE', url: 'api/pricing/pricelists/:priceListId/products/prices' }, + removePrice: { method: 'DELETE', url: 'api/pricing/products/prices' } }); }]) .factory('virtoCommerce.pricingModule.pricelists', ['$resource', function ($resource) { diff --git a/VirtoCommerce.PricingModule.Web/Scripts/widgets/itemPricesWidget.js b/VirtoCommerce.PricingModule.Web/Scripts/widgets/itemPricesWidget.js index 2479425b..cfe74fea 100644 --- a/VirtoCommerce.PricingModule.Web/Scripts/widgets/itemPricesWidget.js +++ b/VirtoCommerce.PricingModule.Web/Scripts/widgets/itemPricesWidget.js @@ -26,18 +26,17 @@ return; var productPricelistsBlade = { - id: "itemPricelists", - itemId: blade.itemId, - item: blade.item, - parentWidgetRefresh: refresh, - title: blade.title, - subtitle: 'pricing.blades.item-pricelists-list.subtitle', - controller: 'virtoCommerce.pricingModule.itemPricelistsListController', - template: 'Modules/$(VirtoCommerce.Pricing)/Scripts/blades/item/item-pricelists-list.tpl.html' + id: "itemPrices", + itemId: blade.itemId, + item: blade.item, + parentWidgetRefresh: refresh, + title: 'pricing.blades.item-prices.title', + titleValues: { name: blade.item.name }, + subtitle: 'pricing.blades.item-prices.subtitle', + controller: 'virtoCommerce.pricingModule.itemPriceListController', + template: 'Modules/$(VirtoCommerce.Pricing)/Scripts/blades/item/item-prices.tpl.html' }; bladeNavigationService.showBlade(productPricelistsBlade, blade); - - }; $scope.$watch("widget.blade.catalog.id", function (id) { diff --git a/VirtoCommerce.PricingModule.Web/VirtoCommerce.PricingModule.Web.csproj b/VirtoCommerce.PricingModule.Web/VirtoCommerce.PricingModule.Web.csproj index 2ea2f760..e9b4883c 100644 --- a/VirtoCommerce.PricingModule.Web/VirtoCommerce.PricingModule.Web.csproj +++ b/VirtoCommerce.PricingModule.Web/VirtoCommerce.PricingModule.Web.csproj @@ -139,6 +139,7 @@ + @@ -159,6 +160,8 @@ + + diff --git a/VirtoCommerce.PricingModule.Web/module.manifest b/VirtoCommerce.PricingModule.Web/module.manifest index c0995b5d..50100e1d 100644 --- a/VirtoCommerce.PricingModule.Web/module.manifest +++ b/VirtoCommerce.PricingModule.Web/module.manifest @@ -5,7 +5,7 @@ 2.13.9 - + Pricing module @@ -25,6 +25,9 @@ VirtoCommerce.PricingModule.Web.dll VirtoCommerce.PricingModule.Web.Module, VirtoCommerce.PricingModule.Web + + +