Skip to content

Commit

Permalink
New blade with prices for product (#5)
Browse files Browse the repository at this point in the history
* New blade with prices for product #5 
* Improved validation for min quantity of product, so user will not be able to add several prices with the same number of min. quantity in same pricelist.
* Fix permissions
* Inline styles moved to stylesheet file
* Grid rows validation fix
* Display name of currency in price list selector
* Empty initial list price instead 0
  • Loading branch information
megafreeman authored Sep 8, 2017
1 parent f36c340 commit e0ffed4
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 16 deletions.
20 changes: 20 additions & 0 deletions VirtoCommerce.PricingModule.Web/Content/css/pricing.css
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,21 @@ public IHttpActionResult DeleteProductPrices(string pricelistId, [FromUri]string
return StatusCode(HttpStatusCode.NoContent);
}

/// <summary>
/// Delete price by ids
/// </summary>
/// <param name="priceIds"></param>
/// <returns></returns>
[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);
}

/// <summary>
/// Delete pricelists
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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?"
Expand Down
225 changes: 225 additions & 0 deletions VirtoCommerce.PricingModule.Web/Scripts/blades/item/item-prices.js
Original file line number Diff line number Diff line change
@@ -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();
}]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<div class="blade-static __expanded">
<div class="form-group">
<label class="form-label head-label">{{'pricing.blades.item-prices.head-label' | translate}} <a href="" ng-click="createNewPricelist()" class="form-edit" va-permission="pricing:read"><i class="form-ico fa fa-pencil"></i></a></label>

<div class="form-input">
<ui-select ng-model="blade.selectedPricelist" class="head-select" required>
<ui-select-match placeholder="{{'pricing.blades.item-prices.pricelist-select-placeholder' | translate}}">{{$select.selected.displayName}}</ui-select-match>
<ui-select-choices repeat="x in blade.pricelistList | filter: $select.search">
<span ng-bind-html="x.displayName | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>

<button class="btn head-button" type="button" ng-disabled="!blade.selectedPricelist" ng-click="blade.addNewPrice(blade.selectedPricelist)">Add new price</button>
</div>
</div>
</div>
<div class="blade-content __large-wide">
<div class="blade-inner">
<div class="inner-block">
<form name="formScope" ng-init="setGridOptions('item-prices-grid', {
data: 'blade.currentEntities',
rowHeight: 60,
rowTemplate: 'list.row.html',
enableCellEdit: false,
enableCellEditOnFocus: true,
editableCellTemplate: 'default-cellTextEditor',
columnDefs: [
{ name: 'name', displayName: 'pricing.blades.item-prices.labels.name', cellTooltip: true, cellTemplate: 'ui-grid/cellNameLinkTemplate', width: 180 },
{ name: 'currency', displayName: 'pricing.blades.item-prices.labels.currency', width: 60 },
{ name: 'catalog', displayName: 'pricing.blades.item-prices.labels.catalog', cellTooltip: true},
{ name: 'list', displayName: 'pricing.blades.item-prices.labels.list-price', editableCellTemplate: 'list-cellTextEditor', validators: {listValidator: true, required: true}, cellTemplate: 'priceCellTitleValidator', enableCellEdit: true, enableHiding: false },
{ name: 'sale', displayName: 'pricing.blades.item-prices.labels.sale-price', editableCellTemplate: 'sale-cellTextEditor', validators: {saleValidator: true}, cellTemplate: 'priceCellTitleValidator', enableCellEdit: true },
{ name: 'minQuantity', displayName: 'pricing.blades.item-prices.labels.min-quantity', editableCellTemplate: 'minQuantity-cellTextEditor', validators: {minQuantityForPricelistValidator: true, required: true}, cellTemplate: 'ui-grid/cellTitleValidator', enableCellEdit: true, enableHiding: false }
]})">
<div class="table-wrapper" ng-if="blade.currentEntities.length" ng-init="setForm(formScope);">
<div ui-grid="gridOptions" ui-grid-cellNav ui-grid-edit ui-grid-validate ui-grid-auto-resize ui-grid-save-state ui-grid-selection ui-grid-resize-columns ui-grid-move-columns ui-grid-height></div>
</div>
</form>
<div class="note" ng-if="!blade.currentEntities.length">{{ 'platform.list.no-data' | translate }}</div>
</div>
</div>
</div>


<script type="text/ng-template" id="ui-grid/cellNameLinkTemplate">
<div class="ui-grid-cell-contents">
<a class="form-input" ng-href='' ng-click='grid.appScope.selectPricelist(row.entity)'>{{row.entity.name}}</a>
</div>
</script>
<script type="text/ng-template" id="ui-grid/cellTitleValidator">
<div class="ui-grid-cell-contents" title="{{grid.validate.getTitleFormattedErrors(row.entity,col.colDef)}}">
<div class="form-editor form-input">
<input class="ng-valid form-input" ng-class="{'ng-invalid' : grid.validate.isInvalid(row.entity,col.colDef)}" value="{{COL_FIELD CUSTOM_FILTERS}}" placeholder="{{'platform.placeholders.n-a' | translate}}" readonly="readonly" />
</div>
</div>
</script>
<script type="text/ng-template" id="priceCellTitleValidator">
<div class="ui-grid-cell-contents" title="{{grid.validate.getTitleFormattedErrors(row.entity,col.colDef)}}">
<div class="form-editor form-input">
<input class="ng-valid form-input" money ng-model="MODEL_COL_FIELD" ng-class="{'ng-invalid' : grid.validate.isInvalid(row.entity,col.colDef)}" placeholder="{{'platform.placeholders.n-a' | translate}}" readonly="readonly" />
</div>
</div>
</script>

<script type="text/ng-template" id="default-cellTextEditor">
<div class="form-editor form-input">
<input class="form-input" ng-model="MODEL_COL_FIELD" ui-grid-editor placeholder="{{'platform.placeholders.n-a' | translate}}" />
</div>
</script>
<script type="text/ng-template" id="list-cellTextEditor">
<div class="form-editor form-input">
<input class="form-input" money required ng-model="MODEL_COL_FIELD" ui-grid-editor ng-class="{'ng-invalid': !grid.appScope.isListPriceValid(row.entity)}" />
</div>
</script>
<script type="text/ng-template" id="sale-cellTextEditor">
<div class="form-editor form-input">
<input class="form-input" money ng-model="MODEL_COL_FIELD" ui-grid-editor ng-class="{'ng-invalid': !grid.appScope.isSalePriceValid(row.entity)}" placeholder="{{'platform.placeholders.n-a' | translate}}">
</div>
</script>
<script type="text/ng-template" id="minQuantity-cellTextEditor">
<div class="form-editor form-input">
<input class="form-input" smart-float num-type="integer" required ng-model="MODEL_COL_FIELD" ui-grid-editor ng-class="{'ng-invalid': !grid.appScope.isUniqueQtyForPricelist(row.entity)}">
</div>
</script>
<script type="text/ng-template" id="list.row.html">
<div ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.uid" ui-grid-one-bind-id-grid="rowRenderIndex + '-' + col.uid + '-cell'" class="ui-grid-cell" ng-class="{'ui-grid-row-header-cell': col.isRowHeader }" role="{{col.isRowHeader ? 'rowheader' : 'gridcell' }}" ui-grid-cell></div>
</script>
Loading

0 comments on commit e0ffed4

Please sign in to comment.