diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeService.cs index ab8f29772..b7a10952d 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeService.cs @@ -4,6 +4,7 @@ using Grand.Domain.Catalog; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; +using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Extensions; using MediatR; @@ -42,7 +43,7 @@ public ProductAttributeService(ICacheBase cacheBase, private readonly IRepository _productRepository; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; - + #endregion #region Methods @@ -52,20 +53,31 @@ public ProductAttributeService(ICacheBase cacheBase, /// /// Gets all product attributes /// + /// Store ident /// Page index /// Page size /// Product attributes - public virtual async Task> GetAllProductAttributes(int pageIndex = 0, + public virtual async Task> GetAllProductAttributes(string storeId = "", int pageIndex = 0, int pageSize = int.MaxValue) { - var key = string.Format(CacheKey.PRODUCTATTRIBUTES_ALL_KEY, pageIndex, pageSize); + var key = string.Format(CacheKey.PRODUCTATTRIBUTES_ALL_KEY, storeId, pageIndex, pageSize); return await _cacheBase.GetAsync(key, () => { var query = from pa in _productAttributeRepository.Table - orderby pa.Name select pa; + + if (!string.IsNullOrEmpty(storeId)) + //Limited to stores rules + query = from p in query + where !p.LimitedToStores || p.Stores.Contains(storeId) + select p; + + query = query.OrderBy(pa => pa.Name); + return Task.FromResult(new PagedList(query, pageIndex, pageSize)); }); + + } /// diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs index d321cd1a7..5d611eea8 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs @@ -150,8 +150,8 @@ public virtual async Task> GetProductsByDiscount(string disc int pageSize = int.MaxValue) { var query = from c in _productRepository.Table - where c.AppliedDiscounts.Any(x => x == discountId) - select c; + where c.AppliedDiscounts.Any(x => x == discountId) + select c; return await PagedList.Create(query, pageIndex, pageSize); } @@ -422,7 +422,7 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat categoryIds.Remove(""); var query = from p in _productRepository.Table - select p; + select p; query = query.Where(p => p.Published && p.VisibleIndividually); @@ -435,15 +435,15 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat //ACL (access control list) var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; + where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) + select p; } if (!string.IsNullOrEmpty(storeId) && !ignoreStore) //Limited to stores rules query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) - select p; + where !p.LimitedToStores || p.Stores.Contains(storeId) + select p; return Convert.ToInt32(query.Count()); } @@ -561,15 +561,20 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat /// Gets products by product attribute /// /// Product attribute identifier + /// Store ident /// Page index /// Page size /// Products - public virtual async Task> GetProductsByProductAttributeId(string productAttributeId, + public virtual async Task> GetProductsByProductAttributeId(string productAttributeId, string storeId = "", int pageIndex = 0, int pageSize = int.MaxValue) { var query = from p in _productRepository.Table - select p; + select p; query = query.Where(x => x.ProductAttributeMappings.Any(y => y.ProductAttributeId == productAttributeId)); + + if (!string.IsNullOrEmpty(storeId)) + query = query.Where(x => x.LimitedToStores || x.Stores.Contains(storeId)); + query = query.OrderBy(x => x.Name); return await PagedList.Create(query, pageIndex, pageSize); @@ -587,7 +592,7 @@ public virtual async Task> GetAssociatedProducts(string parentGro string storeId = "", string vendorId = "", bool showHidden = false) { var query = from p in _productRepository.Table - select p; + select p; query = query.Where(p => p.ParentGroupedProductId == parentGroupedProductId); diff --git a/src/Business/Grand.Business.Catalog/Services/Products/SpecificationAttributeService.cs b/src/Business/Grand.Business.Catalog/Services/Products/SpecificationAttributeService.cs index 256b0ac09..a185e1a20 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/SpecificationAttributeService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/SpecificationAttributeService.cs @@ -4,6 +4,7 @@ using Grand.Domain.Catalog; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; +using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Extensions; using MediatR; @@ -79,15 +80,24 @@ await Task.FromResult(_specificationAttributeRepository.Table /// /// Gets specification attributes /// + /// Store ident /// Page index /// Page size /// Specification attributes - public virtual async Task> GetSpecificationAttributes(int pageIndex = 0, + public virtual async Task> GetSpecificationAttributes(string storeId = "", int pageIndex = 0, int pageSize = int.MaxValue) { var query = from sa in _specificationAttributeRepository.Table - orderby sa.DisplayOrder - select sa; + select sa; + + if (!string.IsNullOrEmpty(storeId)) + //Limited to stores rules + query = from p in query + where !p.LimitedToStores || p.Stores.Contains(storeId) + select p; + + query = query.OrderBy(sa => sa.DisplayOrder).ThenBy(sa => sa.Name); + return await PagedList.Create(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductAttributeService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductAttributeService.cs index 7d8ac1bbf..4d5518b24 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductAttributeService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductAttributeService.cs @@ -19,10 +19,11 @@ public interface IProductAttributeService /// /// Gets all product attributes /// + /// Store ident /// Page index /// Page size /// Product attributes - Task> GetAllProductAttributes(int pageIndex = 0, int pageSize = int.MaxValue); + Task> GetAllProductAttributes(string storeId = "", int pageIndex = 0, int pageSize = int.MaxValue); /// /// Gets a product attribute diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs index 4fa7663af..d76f1c8f1 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs @@ -190,10 +190,11 @@ int GetCategoryProductNumber(Customer customer, IList categoryIds = null /// Gets products by product attribute /// /// Product attribute identifier + /// Store ident /// Page index /// Page size /// Products - Task> GetProductsByProductAttributeId(string productAttributeId, + Task> GetProductsByProductAttributeId(string productAttributeId, string storeId = "", int pageIndex = 0, int pageSize = int.MaxValue); /// diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/ISpecificationAttributeService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/ISpecificationAttributeService.cs index a7b43632e..ab06990aa 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/ISpecificationAttributeService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/ISpecificationAttributeService.cs @@ -27,10 +27,11 @@ public interface ISpecificationAttributeService /// /// Gets specification attributes /// + /// Store ident /// Page index /// Page size /// Specification attributes - Task> GetSpecificationAttributes(int pageIndex = 0, int pageSize = int.MaxValue); + Task> GetSpecificationAttributes(string storeId = "", int pageIndex = 0, int pageSize = int.MaxValue); /// /// Inserts a specification attribute diff --git a/src/Core/Grand.Infrastructure/Caching/Constants/ProductAttributeCacheKey.cs b/src/Core/Grand.Infrastructure/Caching/Constants/ProductAttributeCacheKey.cs index ecfba0699..9a8a07add 100644 --- a/src/Core/Grand.Infrastructure/Caching/Constants/ProductAttributeCacheKey.cs +++ b/src/Core/Grand.Infrastructure/Caching/Constants/ProductAttributeCacheKey.cs @@ -6,10 +6,11 @@ public static partial class CacheKey /// Key for caching /// /// - /// {0} : page index - /// {1} : page size + /// {0} : store ID + /// {1} : page index + /// {2} : page size /// - public static string PRODUCTATTRIBUTES_ALL_KEY => "Grand.productattribute.all-{0}-{1}"; + public static string PRODUCTATTRIBUTES_ALL_KEY => "Grand.productattribute.all-{0}-{1}-{2}"; /// /// Key for caching diff --git a/src/Modules/Grand.Module.Installer/Extensions/PermissionExtensions.cs b/src/Modules/Grand.Module.Installer/Extensions/PermissionExtensions.cs index 9bf265a8a..e4a6d1e4f 100644 --- a/src/Modules/Grand.Module.Installer/Extensions/PermissionExtensions.cs +++ b/src/Modules/Grand.Module.Installer/Extensions/PermissionExtensions.cs @@ -199,6 +199,8 @@ public static IEnumerable DefaultPermissions() Permissions = [ StandardPermission.ManageAccessStoreManagerPanel, StandardPermission.ManageProducts, + StandardPermission.ManageProductAttributes, + StandardPermission.ManageSpecificationAttributes, StandardPermission.ManageFiles, StandardPermission.ManagePictures, StandardPermission.ManageCategories, diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductAttributeController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductAttributeController.cs index 20263e7ec..93bcf5f4b 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductAttributeController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductAttributeController.cs @@ -1,6 +1,5 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Products; -using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Permissions; using Grand.Domain.Seo; @@ -25,7 +24,6 @@ public ProductAttributeController( ILanguageService languageService, ITranslationService translationService, IContextAccessor contextAccessor, - IGroupService groupService, SeoSettings seoSettings) { _productService = productService; @@ -33,7 +31,6 @@ public ProductAttributeController( _languageService = languageService; _translationService = translationService; _contextAccessor = contextAccessor; - _groupService = groupService; _seoSettings = seoSettings; } @@ -46,7 +43,6 @@ public ProductAttributeController( private readonly ILanguageService _languageService; private readonly ITranslationService _translationService; private readonly IContextAccessor _contextAccessor; - private readonly IGroupService _groupService; private readonly SeoSettings _seoSettings; #endregion Fields @@ -70,8 +66,7 @@ public IActionResult List() [HttpPost] public async Task List(DataSourceRequest command) { - var productAttributes = await _productAttributeService - .GetAllProductAttributes(command.Page - 1, command.PageSize); + var productAttributes = await _productAttributeService.GetAllProductAttributes(pageIndex: command.Page - 1, pageSize: command.PageSize); var gridModel = new DataSourceResult { Data = productAttributes.Select(x => x.ToModel()), Total = productAttributes.TotalCount @@ -102,8 +97,6 @@ public async Task Create(ProductAttributeModel model, bool contin string.IsNullOrEmpty(productAttribute.SeName) ? productAttribute.Name : productAttribute.SeName, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, _seoSettings.SeoCharConversion); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; await _productAttributeService.InsertProductAttribute(productAttribute); @@ -154,8 +147,7 @@ public async Task Edit(ProductAttributeModel model, bool continue string.IsNullOrEmpty(productAttribute.SeName) ? productAttribute.Name : productAttribute.SeName, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, _seoSettings.SeoCharConversion); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + await _productAttributeService.UpdateProductAttribute(productAttribute); Success(_translationService.GetResource("Admin.Catalog.Attributes.ProductAttributes.Updated")); @@ -212,6 +204,7 @@ public async Task UsedByProducts(DataSourceRequest command, strin { var orders = await _productService.GetProductsByProductAttributeId( productAttributeId, + "", command.Page - 1, command.PageSize); var gridModel = new DataSourceResult { diff --git a/src/Web/Grand.Web.Admin/Controllers/SpecificationAttributeController.cs b/src/Web/Grand.Web.Admin/Controllers/SpecificationAttributeController.cs index fcb7bc88c..e72ef752d 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SpecificationAttributeController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SpecificationAttributeController.cs @@ -1,6 +1,5 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Products; -using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Domain.Permissions; using Grand.Domain.Seo; @@ -24,7 +23,6 @@ public SpecificationAttributeController( ILanguageService languageService, ITranslationService translationService, IContextAccessor contextAccessor, - IGroupService groupService, IProductService productService, SeoSettings seoSettings) { @@ -32,7 +30,6 @@ public SpecificationAttributeController( _languageService = languageService; _translationService = translationService; _contextAccessor = contextAccessor; - _groupService = groupService; _productService = productService; _seoSettings = seoSettings; } @@ -53,10 +50,6 @@ public async Task UsedByProducts(DataSourceRequest command, strin var searchStoreId = string.Empty; - //limit for store manager - if (!string.IsNullOrEmpty(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - searchStoreId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; - var specificationProducts = new List(); var total = 0; @@ -104,7 +97,6 @@ public async Task UsedByProducts(DataSourceRequest command, strin private readonly ILanguageService _languageService; private readonly ITranslationService _translationService; private readonly IContextAccessor _contextAccessor; - private readonly IGroupService _groupService; private readonly SeoSettings _seoSettings; #endregion Fields @@ -126,8 +118,7 @@ public IActionResult List() [PermissionAuthorizeAction(PermissionActionName.List)] public async Task List(DataSourceRequest command) { - var specificationAttributes = await _specificationAttributeService - .GetSpecificationAttributes(command.Page - 1, command.PageSize); + var specificationAttributes = await _specificationAttributeService.GetSpecificationAttributes(pageIndex: command.Page - 1, pageSize: command.PageSize); var gridModel = new DataSourceResult { Data = specificationAttributes.Select(x => x.ToModel()), Total = specificationAttributes.TotalCount @@ -160,8 +151,7 @@ public async Task Create(SpecificationAttributeModel model, bool ? specificationAttribute.Name : specificationAttribute.SeName, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, _seoSettings.SeoCharConversion); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + await _specificationAttributeService.InsertSpecificationAttribute(specificationAttribute); Success(_translationService.GetResource("Admin.Catalog.Attributes.SpecificationAttributes.Added")); @@ -211,8 +201,7 @@ public async Task Edit(SpecificationAttributeModel model, bool co ? specificationAttribute.Name : specificationAttribute.SeName, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, _seoSettings.SeoCharConversion); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + await _specificationAttributeService.UpdateSpecificationAttribute(specificationAttribute); Success(_translationService.GetResource("Admin.Catalog.Attributes.SpecificationAttributes.Updated")); diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs index 900dd132d..9e75ecc3a 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs @@ -278,7 +278,7 @@ public virtual async Task PrepareProductModel(ProductModel model, Product produc model.AutoAddRequiredProducts = product.AutoAddRequiredProducts; //product attributes - foreach (var productAttribute in await productAttributeService.GetAllProductAttributes()) + foreach (var productAttribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) model.AvailableProductAttributes.Add(new SelectListItem { Text = productAttribute.Name, Value = productAttribute.Id @@ -1372,7 +1372,7 @@ public virtual async Task DeleteBulkEdit(IEnumerable produ var model = new ProductModel.ProductAttributeMappingModel { ProductId = product.Id }; - foreach (var attribute in await productAttributeService.GetAllProductAttributes()) + foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) model.AvailableProductAttribute.Add(new SelectListItem { Value = attribute.Id, Text = attribute.Name @@ -1384,7 +1384,7 @@ public virtual async Task DeleteBulkEdit(IEnumerable produ Product product, ProductAttributeMapping productAttributeMapping) { var model = productAttributeMapping.ToModel(); - foreach (var attribute in await productAttributeService.GetAllProductAttributes()) + foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) model.AvailableProductAttribute.Add(new SelectListItem { Value = attribute.Id, Text = attribute.Name, @@ -1396,7 +1396,7 @@ public virtual async Task DeleteBulkEdit(IEnumerable produ public virtual async Task PrepareProductAttributeMappingModel( ProductModel.ProductAttributeMappingModel model) { - foreach (var attribute in await productAttributeService.GetAllProductAttributes()) + foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) model.AvailableProductAttribute.Add(new SelectListItem { Value = attribute.Id, Text = attribute.Name diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Create.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Create.cshtml new file mode 100644 index 000000000..c624f9e89 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Create.cshtml @@ -0,0 +1,37 @@ +@model ProductAttributeModel +@{ + //page title + ViewBag.Title = Loc["Admin.Catalog.Attributes.ProductAttributes.AddNew"]; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.ProductAttributes.AddNew"] - @Model.Name + + @Html.ActionLink(Loc["Admin.Catalog.Attributes.ProductAttributes.BackToList"], "List") + +
+
+
+ + + +
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Edit.cshtml new file mode 100644 index 000000000..5e5daeec9 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Edit.cshtml @@ -0,0 +1,41 @@ +@model ProductAttributeModel +@{ + //page title + ViewBag.Title = Loc["Admin.Catalog.Attributes.ProductAttributes.EditAttributeDetails"]; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.ProductAttributes.EditAttributeDetails"] - @Model.Name + + @Html.ActionLink(Loc["Admin.Catalog.Attributes.ProductAttributes.BackToList"], "List") + +
+
+
+ + + + @Loc["Admin.Common.Delete"] + + +
+
+
+
+ +
+
+
+
+
+ \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/List.cshtml new file mode 100644 index 000000000..95c4e5726 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/List.cshtml @@ -0,0 +1,77 @@ +@inject AdminAreaSettings adminAreaSettings +@{ + //page title + ViewBag.Title = Loc["Admin.Catalog.Attributes.ProductAttributes"]; +} + +
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.ProductAttributes"] +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabInfo.cshtml new file mode 100644 index 000000000..9539f3658 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabInfo.cshtml @@ -0,0 +1,47 @@ +@using Microsoft.AspNetCore.Mvc.Razor +@model ProductAttributeModel + + + +@{ + Func + template = @
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
; +} + +
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+ \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabPredefinedValues.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabPredefinedValues.cshtml new file mode 100644 index 000000000..5328da963 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabPredefinedValues.cshtml @@ -0,0 +1,144 @@ +@model ProductAttributeModel +@{ + if (!string.IsNullOrEmpty(Model.Id)) + { + + + } + else + { +
+ @Loc["Admin.Catalog.Attributes.ProductAttributes.PredefinedValues.SaveBeforeEdit"] +
+ } +} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabUsedByProducts.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabUsedByProducts.cshtml new file mode 100644 index 000000000..245be339f --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.TabUsedByProducts.cshtml @@ -0,0 +1,63 @@ +@model ProductAttributeModel +@inject AdminAreaSettings adminAreaSettings +@{ +
+ +
+
+
+ +
+ +} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.cshtml new file mode 100644 index 000000000..44465c653 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdate.cshtml @@ -0,0 +1,40 @@ +@model ProductAttributeModel + +
+ + + + + +
+ +
+
+
+ @if (!string.IsNullOrEmpty(Model.Id)) + { + + +
+ +
+
+
+ } + + +
+ +
+
+
+ + +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdatePredefinedProductAttributeValue.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdatePredefinedProductAttributeValue.cshtml new file mode 100644 index 000000000..b004b7240 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/Partials/CreateOrUpdatePredefinedProductAttributeValue.cshtml @@ -0,0 +1,86 @@ +@using Microsoft.AspNetCore.Mvc.Razor +@model PredefinedProductAttributeValueModel + +
+ + + + + +@{ + Func + template = @
+
+ +
+ + +
+
+ +
; +} + +
+ +
+
+ +
+ + +
+
+
+
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ + +
+
+
+
+ \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/PredefinedProductAttributeValueCreatePopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/PredefinedProductAttributeValueCreatePopup.cshtml new file mode 100644 index 000000000..c922648da --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/PredefinedProductAttributeValueCreatePopup.cshtml @@ -0,0 +1,52 @@ +@model PredefinedProductAttributeValueModel +@{ + Layout = ""; +} +
+
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.ProductAttributes.PredefinedValues.AddNew"] +
+
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/PredefinedProductAttributeValueEditPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/PredefinedProductAttributeValueEditPopup.cshtml new file mode 100644 index 000000000..2ea2ba46b --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttribute/PredefinedProductAttributeValueEditPopup.cshtml @@ -0,0 +1,53 @@ +@model PredefinedProductAttributeValueModel +@{ + // layout + Layout = ""; +} +
+
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.ProductAttributes.PredefinedValues.EditValueDetails"] +
+
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Shared/AccessDenied.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Shared/AccessDenied.cshtml new file mode 100644 index 000000000..a427c8113 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Shared/AccessDenied.cshtml @@ -0,0 +1,24 @@ +@model string +@{ + Layout = ""; +} + + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Create.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Create.cshtml new file mode 100644 index 000000000..2c1640e14 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Create.cshtml @@ -0,0 +1,37 @@ +@model SpecificationAttributeModel +@{ + //page title + ViewBag.Title = Loc["Admin.Catalog.Attributes.SpecificationAttributes.AddNew"]; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.SpecificationAttributes.AddNew"] + + @Html.ActionLink(Loc["Admin.Catalog.Attributes.SpecificationAttributes.BackToList"], "List") + +
+
+
+ + + +
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Edit.cshtml new file mode 100644 index 000000000..ee1cd1b15 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Edit.cshtml @@ -0,0 +1,42 @@ +@model SpecificationAttributeModel +@{ + //page title + ViewBag.Title = Loc["Admin.Catalog.Attributes.SpecificationAttributes.EditAttributeDetails"]; +} + +
+ +
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.SpecificationAttributes.EditAttributeDetails"] - @Model.Name + + @Html.ActionLink(Loc["Admin.Catalog.Attributes.SpecificationAttributes.BackToList"], "List") + +
+
+
+ + + + @Loc["Admin.Common.Delete"] + + +
+
+
+
+ +
+
+
+
+
+ \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/List.cshtml new file mode 100644 index 000000000..2f31c98bf --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/List.cshtml @@ -0,0 +1,86 @@ +@inject AdminAreaSettings adminAreaSettings +@{ + //page title + ViewBag.Title = Loc["Admin.Catalog.Attributes.SpecificationAttributes"]; +} + +
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.SpecificationAttributes"] +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/OptionCreatePopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/OptionCreatePopup.cshtml new file mode 100644 index 000000000..f8124efd3 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/OptionCreatePopup.cshtml @@ -0,0 +1,57 @@ +@{ + Layout = ""; +} +@model SpecificationAttributeOptionModel +@{ + //page title + ViewBag.Title = Loc["Admin.Catalog.Attributes.SpecificationAttributes.Options.AddNew"]; +} +
+
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.SpecificationAttributes.Options.AddNew"] +
+
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/OptionEditPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/OptionEditPopup.cshtml new file mode 100644 index 000000000..3ffb92caa --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/OptionEditPopup.cshtml @@ -0,0 +1,58 @@ +@{ + Layout = ""; +} +@model SpecificationAttributeOptionModel +@{ + //page title + ViewBag.Title = Loc["Admin.Catalog.Attributes.SpecificationAttributes.Options.EditOptionDetails"]; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Catalog.Attributes.SpecificationAttributes.Options.EditOptionDetails"] +
+
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabInfo.cshtml new file mode 100644 index 000000000..80a74e56c --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabInfo.cshtml @@ -0,0 +1,51 @@ +@using Microsoft.AspNetCore.Mvc.Razor +@model SpecificationAttributeModel + + + +@{ + Func + template = @
+
+ +
+ + +
+
+ + +
; +} +
+ +
+
+ +
+ + +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+ +
+ + +
+
+
+
+ \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabOptions.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabOptions.cshtml new file mode 100644 index 000000000..72cdbf50d --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabOptions.cshtml @@ -0,0 +1,132 @@ +@model SpecificationAttributeModel + +@{ + if (!string.IsNullOrEmpty(Model.Id)) + { + + + + + } + else + { +
+ @Loc["Admin.Catalog.Attributes.SpecificationAttributes.Options.SaveBeforeEdit"] +
+ } +} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabUsedByProducts.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabUsedByProducts.cshtml new file mode 100644 index 000000000..02dabd36d --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.TabUsedByProducts.cshtml @@ -0,0 +1,68 @@ +@model SpecificationAttributeModel +@inject AdminAreaSettings adminAreaSettings +@{ +
+ +
+
+
+ +
+ +} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.cshtml new file mode 100644 index 000000000..a7d55faa8 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdate.cshtml @@ -0,0 +1,40 @@ +@model SpecificationAttributeModel + +
+ + + + + +
+ +
+
+
+ + +
+ +
+
+
+ @if (!string.IsNullOrEmpty(Model.Id)) + { + + +
+ +
+
+
+ } + + +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdateOption.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdateOption.cshtml new file mode 100644 index 000000000..702b1ecbb --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/SpecificationAttribute/Partials/CreateOrUpdateOption.cshtml @@ -0,0 +1,103 @@ +@using Microsoft.AspNetCore.Mvc.Razor +@model SpecificationAttributeOptionModel + +
+ + +@{ + if (string.IsNullOrEmpty(Model.ColorSquaresRgb)) + { + Model.ColorSquaresRgb = "#FFFFFF"; + } +} + + + + +@{ + Func + template = @
+
+ +
+ + +
+
+ +
; +} +
+ +
+
+ +
+ + +
+
+
+
+
+
+
+ +
+ + +
+
+
+
+ +
+ + +
+
+
+ +
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ + +
+
+
+
+ \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Controllers/CategoryController.cs b/src/Web/Grand.Web.Store/Controllers/CategoryController.cs index 1c968a635..f67ed0255 100644 --- a/src/Web/Grand.Web.Store/Controllers/CategoryController.cs +++ b/src/Web/Grand.Web.Store/Controllers/CategoryController.cs @@ -1,8 +1,6 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Categories; -using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Domain.Catalog; using Grand.Domain.Permissions; using Grand.Infrastructure; using Grand.Web.AdminShared.Extensions; @@ -28,7 +26,6 @@ public CategoryController( ILanguageService languageService, ITranslationService translationService, IContextAccessor contextAccessor, - IGroupService groupService, IPictureViewModelService pictureViewModelService) { _categoryService = categoryService; @@ -36,26 +33,11 @@ public CategoryController( _languageService = languageService; _translationService = translationService; _contextAccessor = contextAccessor; - _groupService = groupService; _pictureViewModelService = pictureViewModelService; } #endregion - #region Utilities - - protected async Task<(bool allow, string message)> CheckAccessToCategory(Category category) - { - if (category == null) return (false, "Category not exists"); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - if (!(!category.LimitedToStores || (category.Stores.Contains(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) && - category.LimitedToStores))) - return (false, "This is not your category"); - return (true, null); - } - - #endregion - #region Fields private readonly ICategoryService _categoryService; @@ -63,7 +45,6 @@ public CategoryController( private readonly ILanguageService _languageService; private readonly ITranslationService _translationService; private readonly IContextAccessor _contextAccessor; - private readonly IGroupService _groupService; private readonly IPictureViewModelService _pictureViewModelService; #endregion @@ -279,6 +260,7 @@ public async Task PicturePopup(PictureModel model) public async Task ProductList(DataSourceRequest command, string categoryId) { var category = await _categoryService.GetCategoryById(categoryId); + if (!category.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) return ErrorForKendoGridJson("This is not your category"); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductAttributeController.cs b/src/Web/Grand.Web.Store/Controllers/ProductAttributeController.cs new file mode 100644 index 000000000..c12259ede --- /dev/null +++ b/src/Web/Grand.Web.Store/Controllers/ProductAttributeController.cs @@ -0,0 +1,413 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Catalog; +using Grand.Domain.Permissions; +using Grand.Domain.Seo; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Extensions; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.Store.Controllers; + +[PermissionAuthorize(PermissionSystemName.ProductAttributes)] +public class ProductAttributeController : BaseStoreController +{ + #region Constructors + + public ProductAttributeController( + IProductService productService, + IProductAttributeService productAttributeService, + ILanguageService languageService, + ITranslationService translationService, + IContextAccessor contextAccessor, + SeoSettings seoSettings) + { + _productService = productService; + _productAttributeService = productAttributeService; + _languageService = languageService; + _translationService = translationService; + _contextAccessor = contextAccessor; + _seoSettings = seoSettings; + } + + #endregion + + #region Fields + + private readonly IProductService _productService; + private readonly IProductAttributeService _productAttributeService; + private readonly ILanguageService _languageService; + private readonly ITranslationService _translationService; + private readonly IContextAccessor _contextAccessor; + private readonly SeoSettings _seoSettings; + + #endregion Fields + + #region Helper Methods + + /// + /// Checks access permissions for a product attribute and handles warnings + /// + /// Product attribute to check + /// True if access is allowed, false if access should be denied + private bool CheckAccessPermission(ProductAttribute productAttribute) + { + if (productAttribute == null) + return false; + + if (!productAttribute.LimitedToStores || (productAttribute.LimitedToStores && + productAttribute.Stores.Contains(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) && + productAttribute.Stores.Count > 1)) + { + Warning(_translationService.GetResource("admin.Catalog.attributes.productattributes.permissions")); + return true; + } + + return productAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId); + } + + #endregion + + #region Methods + + #region Attribute list / create / edit / delete + + //list + public IActionResult Index() + { + return RedirectToAction("List"); + } + + public IActionResult List() + { + return View(); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task List(DataSourceRequest command) + { + var productAttributes = await _productAttributeService + .GetAllProductAttributes(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId, command.Page - 1, command.PageSize); + var gridModel = new DataSourceResult { + Data = productAttributes.Select(x => x.ToModel()), + Total = productAttributes.TotalCount + }; + + return Json(gridModel); + } + + //create + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task Create() + { + var model = new ProductAttributeModel(); + //locales + await AddLocales(_languageService, model.Locales); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Create)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task Create(ProductAttributeModel model, bool continueEditing) + { + if (ModelState.IsValid) + { + model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + + var productAttribute = model.ToEntity(); + productAttribute.SeName = SeoExtensions.GetSeName( + string.IsNullOrEmpty(productAttribute.SeName) ? productAttribute.Name : productAttribute.SeName, + _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, + _seoSettings.SeoCharConversion); + + await _productAttributeService.InsertProductAttribute(productAttribute); + + Success(_translationService.GetResource("Admin.Catalog.Attributes.ProductAttributes.Added")); + return continueEditing + ? RedirectToAction("Edit", new { id = productAttribute.Id }) + : RedirectToAction("List"); + } + + //If we got this far, something failed, redisplay form + return View(model); + } + + //edit + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(id); + if (productAttribute == null) + //No product attribute found with the specified id + return RedirectToAction("List"); + + if (!CheckAccessPermission(productAttribute)) + return RedirectToAction("List"); + + var model = productAttribute.ToModel(); + //locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Name = productAttribute.GetTranslation(x => x.Name, languageId, false); + locale.Description = productAttribute.GetTranslation(x => x.Description, languageId, false); + }); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task Edit(ProductAttributeModel model, bool continueEditing) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(model.Id); + if (productAttribute == null) + //No product attribute found with the specified id + return RedirectToAction("List"); + + if (!productAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("Edit", new { id = productAttribute.Id }); + + if (ModelState.IsValid) + { + model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + productAttribute = model.ToEntity(productAttribute); + productAttribute.SeName = SeoExtensions.GetSeName( + string.IsNullOrEmpty(productAttribute.SeName) ? productAttribute.Name : productAttribute.SeName, + _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, + _seoSettings.SeoCharConversion); + + await _productAttributeService.UpdateProductAttribute(productAttribute); + + Success(_translationService.GetResource("Admin.Catalog.Attributes.ProductAttributes.Updated")); + if (continueEditing) + { + //selected tab + await SaveSelectedTabIndex(); + + return RedirectToAction("Edit", new { id = productAttribute.Id }); + } + + return RedirectToAction("List"); + } + + //If we got this far, something failed, redisplay form + // locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Name = productAttribute.GetTranslation(x => x.Name, languageId, false); + locale.Description = productAttribute.GetTranslation(x => x.Description, languageId, false); + }); + + return View(model); + } + + //delete + [PermissionAuthorizeAction(PermissionActionName.Delete)] + [HttpPost] + public async Task Delete(string id) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(id); + if (productAttribute == null) + //No product attribute found with the specified id + return RedirectToAction("List"); + + if (!productAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("List"); + + if (ModelState.IsValid) + { + await _productAttributeService.DeleteProductAttribute(productAttribute); + Success(_translationService.GetResource("Admin.Catalog.Attributes.ProductAttributes.Deleted")); + return RedirectToAction("List"); + } + + Error(ModelState); + return RedirectToAction("Edit", new { id = productAttribute.Id }); + } + + #endregion + + #region Used by products + + //used by products + [PermissionAuthorizeAction(PermissionActionName.Preview)] + [HttpPost] + public async Task UsedByProducts(DataSourceRequest command, string productAttributeId) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(productAttributeId); + + if (!CheckAccessPermission(productAttribute)) + return RedirectToAction("List"); + + var orders = await _productService.GetProductsByProductAttributeId( + productAttributeId, + _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId, + command.Page - 1, + command.PageSize); + + var gridModel = new DataSourceResult { + Data = orders.Select(x => new ProductAttributeModel.UsedByProductModel { + Id = x.Id, + ProductName = x.Name, + Published = x.Published + }), + Total = orders.TotalCount + }; + return Json(gridModel); + } + + #endregion + + #region Predefined values + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + [HttpPost] + public async Task PredefinedProductAttributeValueList(string productAttributeId, + DataSourceRequest command) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(productAttributeId); + + if (!CheckAccessPermission(productAttribute)) + return View("AccessDenied", _translationService.GetResource("admin.Catalog.attributes.productattributes.permissions")); + + var values = productAttribute.PredefinedProductAttributeValues; + var gridModel = new DataSourceResult { + Data = values.Select(x => x.ToModel()), + Total = values.Count + }; + + return Json(gridModel); + } + + //create + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task PredefinedProductAttributeValueCreatePopup(string productAttributeId) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(productAttributeId); + if (productAttribute == null) + throw new ArgumentException("No product attribute found with the specified id"); + + if (!productAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("AccessDenied", _translationService.GetResource("admin.Catalog.attributes.productattributes.permissions")); + + var model = new PredefinedProductAttributeValueModel { + ProductAttributeId = productAttributeId + }; + + //locales + await AddLocales(_languageService, model.Locales); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task PredefinedProductAttributeValueCreatePopup( + PredefinedProductAttributeValueModel model) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(model.ProductAttributeId); + if (productAttribute == null) + throw new ArgumentException("No product attribute found with the specified id"); + + if (!productAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("AccessDenied", _translationService.GetResource("admin.Catalog.attributes.productattributes.permissions")); + + if (ModelState.IsValid) + { + var ppav = model.ToEntity(); + productAttribute.PredefinedProductAttributeValues.Add(ppav); + await _productAttributeService.UpdateProductAttribute(productAttribute); + return Content(""); + } + + //If we got this far, something failed, redisplay form + return View(model); + } + + //edit + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task PredefinedProductAttributeValueEditPopup(string id, string productAttributeId) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(productAttributeId); + + if (!CheckAccessPermission(productAttribute)) + return View("AccessDenied", _translationService.GetResource("admin.Catalog.attributes.productattributes.permissions")); + + var ppav = productAttribute.PredefinedProductAttributeValues.FirstOrDefault(x => x.Id == id); + if (ppav == null) + throw new ArgumentException("No product attribute value found with the specified id"); + + var model = ppav.ToModel(); + //locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Name = ppav.GetTranslation(x => x.Name, languageId, false); + }); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task PredefinedProductAttributeValueEditPopup( + PredefinedProductAttributeValueModel model) + { + var productAttribute = await _productAttributeService.GetProductAttributeById(model.ProductAttributeId); + + if (!productAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("AccessDenied", _translationService.GetResource("admin.Catalog.attributes.productattributes.permissions")); + + var ppav = productAttribute.PredefinedProductAttributeValues.FirstOrDefault(x => x.Id == model.Id); + if (ppav == null) + throw new ArgumentException("No product attribute value found with the specified id"); + + if (ModelState.IsValid) + { + ppav = model.ToEntity(ppav); + await _productAttributeService.UpdateProductAttribute(productAttribute); + return Content(""); + } + + //If we got this far, something failed, redisplay form + return View(model); + } + + //delete + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task PredefinedProductAttributeValueDelete(string id) + { + var productAttributes = await _productAttributeService.GetAllProductAttributes(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId); + var productAttribute = productAttributes.FirstOrDefault(x => x.PredefinedProductAttributeValues.Any(p => p.Id == id)); + + if (productAttribute == null) + throw new ArgumentException("No product attribute found with the specified id"); + + if (!productAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("AccessDenied", _translationService.GetResource("admin.Catalog.attributes.productattributes.permissions")); + + if (ModelState.IsValid) + { + var ppav = productAttribute.PredefinedProductAttributeValues.FirstOrDefault(x => x.Id == id); + if (ppav == null) + throw new ArgumentException("No predefined product attribute value found with the specified id"); + productAttribute.PredefinedProductAttributeValues.Remove(ppav); + await _productAttributeService.UpdateProductAttribute(productAttribute); + return new JsonResult(""); + } + + return ErrorForKendoGridJson(ModelState); + } + + #endregion + + #endregion +} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Controllers/ProductController.cs b/src/Web/Grand.Web.Store/Controllers/ProductController.cs index ad8ad745b..02d532b6a 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductController.cs @@ -1317,7 +1317,7 @@ private async Task> PrepareAvailableAttributes( ISpecificationAttributeService specificationAttributeService) { var availableSpecificationAttributes = new List(); - foreach (var sa in await specificationAttributeService.GetSpecificationAttributes()) + foreach (var sa in await specificationAttributeService.GetSpecificationAttributes(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) availableSpecificationAttributes.Add(new SelectListItem { Text = sa.Name, Value = sa.Id diff --git a/src/Web/Grand.Web.Store/Controllers/SpecificationAttributeController.cs b/src/Web/Grand.Web.Store/Controllers/SpecificationAttributeController.cs new file mode 100644 index 000000000..6f619571d --- /dev/null +++ b/src/Web/Grand.Web.Store/Controllers/SpecificationAttributeController.cs @@ -0,0 +1,455 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Catalog; +using Grand.Domain.Permissions; +using Grand.Domain.Seo; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Extensions; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authorization; +using Grand.Web.Store.Controllers; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.Admin.Controllers; + +[PermissionAuthorize(PermissionSystemName.SpecificationAttributes)] +public class SpecificationAttributeController : BaseStoreController +{ + + #region Fields + + private readonly ISpecificationAttributeService _specificationAttributeService; + private readonly IProductService _productService; + private readonly ILanguageService _languageService; + private readonly ITranslationService _translationService; + private readonly IContextAccessor _contextAccessor; + private readonly SeoSettings _seoSettings; + + #endregion Fields + + #region Constructors + + public SpecificationAttributeController( + ISpecificationAttributeService specificationAttributeService, + ILanguageService languageService, + ITranslationService translationService, + IContextAccessor contextAccessor, + IProductService productService, + SeoSettings seoSettings) + { + _specificationAttributeService = specificationAttributeService; + _languageService = languageService; + _translationService = translationService; + _contextAccessor = contextAccessor; + _productService = productService; + _seoSettings = seoSettings; + } + + #endregion + + #region Used by products + + //used by products + [PermissionAuthorizeAction(PermissionActionName.Preview)] + [HttpPost] + public async Task UsedByProducts(DataSourceRequest command, string specificationAttributeId) + { + var specification = await _specificationAttributeService.GetSpecificationAttributeById(specificationAttributeId); + if (specification == null) + throw new ArgumentException("No specification found with the specified id"); + + if (!CheckAccessPermission(specification)) + return RedirectToAction("List"); + + var searchStoreId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + var specificationProducts = new List(); + var total = 0; + + var searchspecificationOptions = specification.SpecificationAttributeOptions.Select(x => x.Id).ToList(); + if (searchspecificationOptions.Any()) + { + var products = (await _productService.SearchProducts( + storeId: searchStoreId, + specificationOptions: searchspecificationOptions, + pageIndex: command.Page - 1, + pageSize: command.PageSize, + showHidden: true + )).products; + + total = products.TotalCount; + + foreach (var item in products) + { + var specOption = item.ProductSpecificationAttributes.FirstOrDefault(x => x.SpecificationAttributeId == specificationAttributeId); + specificationProducts.Add(new SpecificationAttributeModel.UsedByProductModel { + Id = item.Id, + ProductName = item.Name, + OptionName = specification.SpecificationAttributeOptions + .FirstOrDefault(x => x.Id == specOption?.SpecificationAttributeOptionId)?.Name, + Published = item.Published + }); + } + } + + var gridModel = new DataSourceResult { + Data = specificationProducts, + Total = total + }; + return Json(gridModel); + } + + #endregion + + #region Helper Methods + + /// + /// Checks access permissions for a specification attribute and handles warnings + /// + /// + /// True if access is allowed, false if access should be denied + private bool CheckAccessPermission(SpecificationAttribute specificationAttribute) + { + if (specificationAttribute == null) + return false; + + if (!specificationAttribute.LimitedToStores || (specificationAttribute.LimitedToStores && + specificationAttribute.Stores.Contains(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) && + specificationAttribute.Stores.Count > 1)) + { + Warning(_translationService.GetResource("admin.catalog.attributes.specificationattributes.permissions")); + return true; + } + + return specificationAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId); + } + + #endregion + + #region Specification attributes + + //list + public IActionResult Index() + { + return RedirectToAction("List"); + } + + public IActionResult List() + { + return View(); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.List)] + public async Task List(DataSourceRequest command) + { + var specificationAttributes = await _specificationAttributeService + .GetSpecificationAttributes(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId, command.Page - 1, command.PageSize); + var gridModel = new DataSourceResult { + Data = specificationAttributes.Select(x => x.ToModel()), + Total = specificationAttributes.TotalCount + }; + + return Json(gridModel); + } + + //create + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task Create() + { + var model = new SpecificationAttributeModel(); + //locales + await AddLocales(_languageService, model.Locales); + + return View(model); + } + + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task Create(SpecificationAttributeModel model, bool continueEditing) + { + if (ModelState.IsValid) + { + model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + + var specificationAttribute = model.ToEntity(); + specificationAttribute.SeName = SeoExtensions.GetSeName( + string.IsNullOrEmpty(specificationAttribute.SeName) + ? specificationAttribute.Name + : specificationAttribute.SeName, _seoSettings.ConvertNonWesternChars, + _seoSettings.AllowUnicodeCharsInUrls, _seoSettings.SeoCharConversion); + + await _specificationAttributeService.InsertSpecificationAttribute(specificationAttribute); + + Success(_translationService.GetResource("Admin.Catalog.Attributes.SpecificationAttributes.Added")); + return continueEditing + ? RedirectToAction("Edit", new { id = specificationAttribute.Id }) + : RedirectToAction("List"); + } + + //If we got this far, something failed, redisplay form + return View(model); + } + + //edit + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var specificationAttribute = await _specificationAttributeService.GetSpecificationAttributeById(id); + if (specificationAttribute == null) + //No specification attribute found with the specified id + return RedirectToAction("List"); + + if (!CheckAccessPermission(specificationAttribute)) + return RedirectToAction("List"); + + var model = specificationAttribute.ToModel(); + //locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Name = specificationAttribute.GetTranslation(x => x.Name, languageId, false); + }); + + return View(model); + } + + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task Edit(SpecificationAttributeModel model, bool continueEditing) + { + var specificationAttribute = await _specificationAttributeService.GetSpecificationAttributeById(model.Id); + if (specificationAttribute == null) + //No specification attribute found with the specified id + return RedirectToAction("List"); + + if (!specificationAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("List"); + + if (ModelState.IsValid) + { + model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + + specificationAttribute = model.ToEntity(specificationAttribute); + specificationAttribute.SeName = SeoExtensions.GetSeName( + string.IsNullOrEmpty(specificationAttribute.SeName) + ? specificationAttribute.Name + : specificationAttribute.SeName, _seoSettings.ConvertNonWesternChars, + _seoSettings.AllowUnicodeCharsInUrls, _seoSettings.SeoCharConversion); + + await _specificationAttributeService.UpdateSpecificationAttribute(specificationAttribute); + + Success(_translationService.GetResource("Admin.Catalog.Attributes.SpecificationAttributes.Updated")); + + if (continueEditing) + { + //selected tab + await SaveSelectedTabIndex(); + + return RedirectToAction("Edit", new { id = specificationAttribute.Id }); + } + + return RedirectToAction("List"); + } + + //If we got this far, something failed, redisplay form + //locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Name = specificationAttribute.GetTranslation(x => x.Name, languageId, false); + }); + return View(model); + } + + //delete + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Delete)] + public async Task Delete(string id) + { + var specificationAttribute = await _specificationAttributeService.GetSpecificationAttributeById(id); + if (specificationAttribute == null) + //No specification attribute found with the specified id + return RedirectToAction("List"); + + if (!specificationAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("List"); + + if (ModelState.IsValid) + { + await _specificationAttributeService.DeleteSpecificationAttribute(specificationAttribute); + + Success(_translationService.GetResource("Admin.Catalog.Attributes.SpecificationAttributes.Deleted")); + return RedirectToAction("List"); + } + + Error(ModelState); + return RedirectToAction("Edit", new { id = specificationAttribute.Id }); + } + + #endregion + + #region Specification attribute options + + //list + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task OptionList(string specificationAttributeId, DataSourceRequest command) + { + var specificationAttribute = await _specificationAttributeService.GetSpecificationAttributeById(specificationAttributeId); + + if (!CheckAccessPermission(specificationAttribute)) + return Json(""); + + var options = specificationAttribute.SpecificationAttributeOptions.OrderBy(x => x.DisplayOrder); + + var gridModel = new DataSourceResult { + Data = options.Select(x => + { + var model = x.ToModel(); + //in order to save performance to do not check whether a product is deleted, etc + model.NumberOfAssociatedProducts = _specificationAttributeService + .GetProductSpecificationAttributeCount("", x.Id); + return model; + }), + Total = options.Count() + }; + + return Json(gridModel); + } + + //create + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task OptionCreatePopup(string specificationAttributeId) + { + var model = new SpecificationAttributeOptionModel { + SpecificationAttributeId = specificationAttributeId + }; + //locales + await AddLocales(_languageService, model.Locales); + return View(model); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task OptionCreatePopup(SpecificationAttributeOptionModel model) + { + var specificationAttribute = await _specificationAttributeService.GetSpecificationAttributeById(model.SpecificationAttributeId); + if (specificationAttribute == null) + //No specification attribute found with the specified id + return RedirectToAction("List"); + + if (!specificationAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("AccessDenied", _translationService.GetResource("admin.catalog.attributes.specificationattributes.permissions")); + + if (ModelState.IsValid) + { + var sao = model.ToEntity(); + sao.SeName = SeoExtensions.GetSeName(string.IsNullOrEmpty(sao.SeName) ? sao.Name : sao.SeName, + _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, + _seoSettings.SeoCharConversion); + //clear "Color" values if it's disabled + if (!model.EnableColorSquaresRgb) + sao.ColorSquaresRgb = null; + + specificationAttribute.SpecificationAttributeOptions.Add(sao); + await _specificationAttributeService.UpdateSpecificationAttribute(specificationAttribute); + return Content(""); + } + + //If we got this far, something failed, redisplay form + return View(model); + } + + //edit + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task OptionEditPopup(string id) + { + var specificationAttribute = await _specificationAttributeService.GetSpecificationAttributeByOptionId(id); + if (specificationAttribute == null) + //No specification attribute found with the specified id + return RedirectToAction("List"); + + var sao = specificationAttribute.SpecificationAttributeOptions.FirstOrDefault(x => x.Id == id); + + if (!CheckAccessPermission(specificationAttribute)) + return View("AccessDenied", _translationService.GetResource("admin.catalog.attributes.specificationattributes.permissions")); + + if (sao == null) + //No specification attribute option found with the specified id + return RedirectToAction("List"); + + var model = sao.ToModel(); + model.EnableColorSquaresRgb = !string.IsNullOrEmpty(sao.ColorSquaresRgb); + //locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Name = sao.GetTranslation(x => x.Name, languageId, false); + }); + + return View(model); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task OptionEditPopup(SpecificationAttributeOptionModel model) + { + var specificationAttribute = await _specificationAttributeService.GetSpecificationAttributeByOptionId(model.Id); + + if (!specificationAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("AccessDenied", _translationService.GetResource("admin.catalog.attributes.specificationattributes.permissions")); + + var sao = specificationAttribute.SpecificationAttributeOptions.FirstOrDefault(x => x.Id == model.Id); + if (sao == null) + //No specification attribute option found with the specified id + return RedirectToAction("List"); + + if (ModelState.IsValid) + { + sao = model.ToEntity(sao); + sao.SeName = SeoExtensions.GetSeName(string.IsNullOrEmpty(sao.SeName) ? sao.Name : sao.SeName, + _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, + _seoSettings.SeoCharConversion); + + //clear "Color" values if it's disabled + if (!model.EnableColorSquaresRgb) + sao.ColorSquaresRgb = null; + + await _specificationAttributeService.UpdateSpecificationAttribute(specificationAttribute); + return Content(""); + } + + //If we got this far, something failed, redisplay form + return View(model); + } + + //delete + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task OptionDelete(string id) + { + if (ModelState.IsValid) + { + var specificationAttribute = await _specificationAttributeService.GetSpecificationAttributeByOptionId(id); + + if (!specificationAttribute.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("AccessDenied", _translationService.GetResource("admin.catalog.attributes.specificationattributes.permissions")); + + var sao = specificationAttribute.SpecificationAttributeOptions.FirstOrDefault(x => x.Id == id); + if (sao == null) + throw new ArgumentException("No specification attribute option found with the specified id"); + + await _specificationAttributeService.DeleteSpecificationAttributeOption(sao); + + return new JsonResult(""); + } + + return ErrorForKendoGridJson(ModelState); + } + + #endregion +} \ No newline at end of file diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml index 4f8c27afa..da1c65d7c 100644 Binary files a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml and b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml differ