From 1749555ed59f0ea8e232d1c8c7a9b99b0d3979f8 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 12 Oct 2025 18:44:53 +0200 Subject: [PATCH 1/3] faszoooom --- .../Components/FileUploadGridComponent.cs | 16 + .../Admin/Components/TestGridComponent.cs | 22 + .../Controllers/CustomOrderController.cs | 3 +- .../Controllers/CustomProductController.cs | 3863 +++++++++++++++++ .../Controllers/FileManagerController.cs | 12 + .../FileManagerScriptsApiController.cs | 44 + .../Controllers/ManagementPageController.cs | 43 +- .../Admin/Models/Catalog/ProductListModel.cs | 10 + .../Areas/Admin/Models/TestGridModel.cs | 185 +- .../Areas/Admin/Models/TestPageModel.cs | 5 + .../Order/FileUploadGridComponent.cshtml | 118 + .../Areas/Admin/Views/Order/List.cshtml | 4 +- .../ShippingDocumentGridComponent.cshtml | 354 +- .../Views/Order/TestGridComponent.cshtml | 117 + .../Areas/Admin/Views/Product/List.cshtml | 466 ++ Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs | 2 +- .../Infrastructure/PluginNopStartup.cs | 6 +- .../Infrastructure/RouteProvider.cs | 10 + .../Nop.Plugin.Misc.FruitBankPlugin.csproj | 11 + 19 files changed, 5043 insertions(+), 248 deletions(-) create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/FileUploadGridComponent.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/TestGridComponent.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomProductController.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerController.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/Catalog/ProductListModel.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Product/List.cshtml diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/FileUploadGridComponent.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/FileUploadGridComponent.cs new file mode 100644 index 0000000..6894a0d --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/FileUploadGridComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components +{ + [ViewComponent(Name = "FileUploadGridComponent")] + public class FileUploadGridComponent : ViewComponent + { + public async Task InvokeAsync(TestGridModel model) + { + // Here you can fetch data for this grid if needed + // For demo, just pass the model + return View(model.ViewComponentName +".cshtml", model); + } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/TestGridComponent.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/TestGridComponent.cs new file mode 100644 index 0000000..a7f5782 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/TestGridComponent.cs @@ -0,0 +1,22 @@ +// ViewComponent Class +using DocumentFormat.OpenXml.Wordprocessing; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models; +using Nop.Services.Plugins; +using Org.BouncyCastle.Asn1.Ocsp; +using System.ComponentModel; +using System.Text.Json; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components +{ + [ViewComponent(Name = "TestChildGrid")] + public class TestChildGridViewComponent : ViewComponent + { + public IViewComponentResult Invoke(TestGridModel model) + { + return View(model.ViewComponentLocation, model); + } + } +} + diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs index a24945c..ae8ae70 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs @@ -86,7 +86,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers public async Task OrderList(OrderSearchModel searchModel) { //prepare model - var orderListModel = await GetOrderListModelByFilter(searchModel); + //var orderListModel = await GetOrderListModelByFilter(searchModel); + var orderListModel = new OrderListModel(); var valami = Json(orderListModel); Console.WriteLine(valami); diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomProductController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomProductController.cs new file mode 100644 index 0000000..1fc5d10 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomProductController.cs @@ -0,0 +1,3863 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Nop.Core; +using Nop.Core.Domain.Catalog; +using Nop.Core.Domain.Common; +using Nop.Core.Domain.Customers; +using Nop.Core.Domain.Directory; +using Nop.Core.Domain.Discounts; +using Nop.Core.Domain.Media; +using Nop.Core.Domain.Orders; +using Nop.Core.Domain.Tax; +using Nop.Core.Domain.Vendors; +using Nop.Core.Http; +using Nop.Core.Infrastructure; +using Nop.Services.Catalog; +using Nop.Services.Common; +using Nop.Services.Configuration; +using Nop.Services.Customers; +using Nop.Services.Directory; +using Nop.Services.Discounts; +using Nop.Services.ExportImport; +using Nop.Services.Localization; +using Nop.Services.Logging; +using Nop.Services.Media; +using Nop.Services.Messages; +using Nop.Services.Orders; +using Nop.Services.Security; +using Nop.Services.Seo; +using Nop.Services.Shipping; +using Nop.Web.Areas.Admin.Factories; +using Nop.Web.Areas.Admin.Infrastructure.Mapper.Extensions; +using Nop.Web.Areas.Admin.Models.Catalog; +using Nop.Web.Framework.Controllers; +using Nop.Web.Framework.Mvc; +using Nop.Web.Framework.Mvc.Filters; +using Nop.Web.Framework.Mvc.ModelBinding; +using Nop.Web.Framework.Validators; +using System.Text; + +namespace Nop.Web.Areas.Admin.Controllers; + +public partial class CustomProductController : BaseAdminController +{ + #region Fields + + protected readonly AdminAreaSettings _adminAreaSettings; + protected readonly IAclService _aclService; + protected readonly IBackInStockSubscriptionService _backInStockSubscriptionService; + protected readonly ICategoryService _categoryService; + protected readonly ICopyProductService _copyProductService; + protected readonly ICurrencyService _currencyService; + protected readonly ICustomerActivityService _customerActivityService; + protected readonly ICustomerService _customerService; + protected readonly IDiscountService _discountService; + protected readonly IDownloadService _downloadService; + protected readonly IExportManager _exportManager; + protected readonly IGenericAttributeService _genericAttributeService; + protected readonly IHttpClientFactory _httpClientFactory; + protected readonly IImportManager _importManager; + protected readonly ILanguageService _languageService; + protected readonly ILocalizationService _localizationService; + protected readonly ILocalizedEntityService _localizedEntityService; + protected readonly IManufacturerService _manufacturerService; + protected readonly INopFileProvider _fileProvider; + protected readonly INotificationService _notificationService; + protected readonly IPdfService _pdfService; + protected readonly IPermissionService _permissionService; + protected readonly IPictureService _pictureService; + protected readonly IProductAttributeFormatter _productAttributeFormatter; + protected readonly IProductAttributeParser _productAttributeParser; + protected readonly IProductAttributeService _productAttributeService; + protected readonly IProductModelFactory _productModelFactory; + protected readonly IProductService _productService; + protected readonly IProductTagService _productTagService; + protected readonly ISettingService _settingService; + protected readonly IShippingService _shippingService; + protected readonly IShoppingCartService _shoppingCartService; + protected readonly ISpecificationAttributeService _specificationAttributeService; + protected readonly IStoreContext _storeContext; + protected readonly IUrlRecordService _urlRecordService; + protected readonly IVideoService _videoService; + protected readonly IWebHelper _webHelper; + protected readonly IWorkContext _workContext; + protected readonly CurrencySettings _currencySettings; + protected readonly TaxSettings _taxSettings; + protected readonly VendorSettings _vendorSettings; + private static readonly char[] _separator = [',']; + + #endregion + + #region Ctor + + public CustomProductController(AdminAreaSettings adminAreaSettings, + IAclService aclService, + IBackInStockSubscriptionService backInStockSubscriptionService, + ICategoryService categoryService, + ICopyProductService copyProductService, + ICurrencyService currencyService, + ICustomerActivityService customerActivityService, + ICustomerService customerService, + IDiscountService discountService, + IDownloadService downloadService, + IExportManager exportManager, + IGenericAttributeService genericAttributeService, + IHttpClientFactory httpClientFactory, + IImportManager importManager, + ILanguageService languageService, + ILocalizationService localizationService, + ILocalizedEntityService localizedEntityService, + IManufacturerService manufacturerService, + INopFileProvider fileProvider, + INotificationService notificationService, + IPdfService pdfService, + IPermissionService permissionService, + IPictureService pictureService, + IProductAttributeFormatter productAttributeFormatter, + IProductAttributeParser productAttributeParser, + IProductAttributeService productAttributeService, + IProductModelFactory productModelFactory, + IProductService productService, + IProductTagService productTagService, + ISettingService settingService, + IShippingService shippingService, + IShoppingCartService shoppingCartService, + ISpecificationAttributeService specificationAttributeService, + IStoreContext storeContext, + IUrlRecordService urlRecordService, + IVideoService videoService, + IWebHelper webHelper, + IWorkContext workContext, + CurrencySettings currencySettings, + TaxSettings taxSettings, + VendorSettings vendorSettings) + { + _adminAreaSettings = adminAreaSettings; + _aclService = aclService; + _backInStockSubscriptionService = backInStockSubscriptionService; + _categoryService = categoryService; + _copyProductService = copyProductService; + _currencyService = currencyService; + _customerActivityService = customerActivityService; + _customerService = customerService; + _discountService = discountService; + _downloadService = downloadService; + _exportManager = exportManager; + _genericAttributeService = genericAttributeService; + _httpClientFactory = httpClientFactory; + _importManager = importManager; + _languageService = languageService; + _localizationService = localizationService; + _localizedEntityService = localizedEntityService; + _manufacturerService = manufacturerService; + _fileProvider = fileProvider; + _notificationService = notificationService; + _pdfService = pdfService; + _permissionService = permissionService; + _pictureService = pictureService; + _productAttributeFormatter = productAttributeFormatter; + _productAttributeParser = productAttributeParser; + _productAttributeService = productAttributeService; + _productModelFactory = productModelFactory; + _productService = productService; + _productTagService = productTagService; + _settingService = settingService; + _shippingService = shippingService; + _shoppingCartService = shoppingCartService; + _specificationAttributeService = specificationAttributeService; + _storeContext = storeContext; + _urlRecordService = urlRecordService; + _videoService = videoService; + _webHelper = webHelper; + _workContext = workContext; + _currencySettings = currencySettings; + _taxSettings = taxSettings; + _vendorSettings = vendorSettings; + } + + #endregion + + #region Utilities + + protected virtual async Task UpdateLocalesAsync(Product product, ProductModel model) + { + foreach (var localized in model.Locales) + { + await _localizedEntityService.SaveLocalizedValueAsync(product, + x => x.Name, + localized.Name, + localized.LanguageId); + await _localizedEntityService.SaveLocalizedValueAsync(product, + x => x.ShortDescription, + localized.ShortDescription, + localized.LanguageId); + await _localizedEntityService.SaveLocalizedValueAsync(product, + x => x.FullDescription, + localized.FullDescription, + localized.LanguageId); + await _localizedEntityService.SaveLocalizedValueAsync(product, + x => x.MetaKeywords, + localized.MetaKeywords, + localized.LanguageId); + await _localizedEntityService.SaveLocalizedValueAsync(product, + x => x.MetaDescription, + localized.MetaDescription, + localized.LanguageId); + await _localizedEntityService.SaveLocalizedValueAsync(product, + x => x.MetaTitle, + localized.MetaTitle, + localized.LanguageId); + + //search engine name + var seName = await _urlRecordService.ValidateSeNameAsync(product, localized.SeName, localized.Name, false); + await _urlRecordService.SaveSlugAsync(product, seName, localized.LanguageId); + } + } + + protected virtual async Task UpdateLocalesAsync(ProductTag productTag, ProductTagModel model) + { + foreach (var localized in model.Locales) + { + await _localizedEntityService.SaveLocalizedValueAsync(productTag, + x => x.Name, + localized.Name, + localized.LanguageId); + + var seName = await _urlRecordService.ValidateSeNameAsync(productTag, string.Empty, localized.Name, false); + await _urlRecordService.SaveSlugAsync(productTag, seName, localized.LanguageId); + } + } + + protected virtual async Task UpdateLocalesAsync(ProductAttributeMapping pam, ProductAttributeMappingModel model) + { + foreach (var localized in model.Locales) + { + await _localizedEntityService.SaveLocalizedValueAsync(pam, + x => x.TextPrompt, + localized.TextPrompt, + localized.LanguageId); + await _localizedEntityService.SaveLocalizedValueAsync(pam, + x => x.DefaultValue, + localized.DefaultValue, + localized.LanguageId); + } + } + + protected virtual async Task UpdateLocalesAsync(ProductAttributeValue pav, ProductAttributeValueModel model) + { + foreach (var localized in model.Locales) + { + await _localizedEntityService.SaveLocalizedValueAsync(pav, + x => x.Name, + localized.Name, + localized.LanguageId); + } + } + + protected virtual async Task UpdatePictureSeoNamesAsync(Product product) + { + foreach (var pp in await _productService.GetProductPicturesByProductIdAsync(product.Id)) + await _pictureService.SetSeoFilenameAsync(pp.PictureId, await _pictureService.GetPictureSeNameAsync(product.Name)); + } + + protected virtual async Task SaveCategoryMappingsAsync(Product product, ProductModel model) + { + var existingProductCategories = await _categoryService.GetProductCategoriesByProductIdAsync(product.Id, true); + + //delete categories + foreach (var existingProductCategory in existingProductCategories) + if (!model.SelectedCategoryIds.Contains(existingProductCategory.CategoryId)) + await _categoryService.DeleteProductCategoryAsync(existingProductCategory); + + //add categories + foreach (var categoryId in model.SelectedCategoryIds) + { + var category = await _categoryService.GetCategoryByIdAsync(categoryId); + if (category is null) + continue; + + if (!await _categoryService.CanVendorAddProductsAsync(category)) + continue; + + if (_categoryService.FindProductCategory(existingProductCategories, product.Id, categoryId) == null) + { + //find next display order + var displayOrder = 1; + var existingCategoryMapping = await _categoryService.GetProductCategoriesByCategoryIdAsync(categoryId, showHidden: true); + if (existingCategoryMapping.Any()) + displayOrder = existingCategoryMapping.Max(x => x.DisplayOrder) + 1; + await _categoryService.InsertProductCategoryAsync(new ProductCategory + { + ProductId = product.Id, + CategoryId = categoryId, + DisplayOrder = displayOrder + }); + } + } + } + + protected virtual async Task SaveManufacturerMappingsAsync(Product product, ProductModel model) + { + var existingProductManufacturers = await _manufacturerService.GetProductManufacturersByProductIdAsync(product.Id, true); + + //delete manufacturers + foreach (var existingProductManufacturer in existingProductManufacturers) + if (!model.SelectedManufacturerIds.Contains(existingProductManufacturer.ManufacturerId)) + await _manufacturerService.DeleteProductManufacturerAsync(existingProductManufacturer); + + //add manufacturers + foreach (var manufacturerId in model.SelectedManufacturerIds) + { + if (_manufacturerService.FindProductManufacturer(existingProductManufacturers, product.Id, manufacturerId) == null) + { + //find next display order + var displayOrder = 1; + var existingManufacturerMapping = await _manufacturerService.GetProductManufacturersByManufacturerIdAsync(manufacturerId, showHidden: true); + if (existingManufacturerMapping.Any()) + displayOrder = existingManufacturerMapping.Max(x => x.DisplayOrder) + 1; + await _manufacturerService.InsertProductManufacturerAsync(new ProductManufacturer + { + ProductId = product.Id, + ManufacturerId = manufacturerId, + DisplayOrder = displayOrder + }); + } + } + } + + protected virtual async Task SaveDiscountMappingsAsync(Product product, ProductModel model) + { + var allDiscounts = await _discountService.GetAllDiscountsAsync(DiscountType.AssignedToSkus, showHidden: true, isActive: null); + + foreach (var discount in allDiscounts) + { + if (model.SelectedDiscountIds != null && model.SelectedDiscountIds.Contains(discount.Id)) + { + //new discount + if (await _productService.GetDiscountAppliedToProductAsync(product.Id, discount.Id) is null) + await _productService.InsertDiscountProductMappingAsync(new DiscountProductMapping { EntityId = product.Id, DiscountId = discount.Id }); + } + else + { + //remove discount + if (await _productService.GetDiscountAppliedToProductAsync(product.Id, discount.Id) is DiscountProductMapping discountProductMapping) + await _productService.DeleteDiscountProductMappingAsync(discountProductMapping); + } + } + + await _productService.UpdateProductAsync(product); + } + + protected virtual async Task GetAttributesXmlForProductAttributeCombinationAsync(IFormCollection form, List warnings, int productId) + { + var attributesXml = string.Empty; + + //get product attribute mappings (exclude non-combinable attributes) + var attributes = (await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(productId)) + .Where(productAttributeMapping => !productAttributeMapping.IsNonCombinable()).ToList(); + + foreach (var attribute in attributes) + { + var controlId = $"{NopCatalogDefaults.ProductAttributePrefix}{attribute.Id}"; + StringValues ctrlAttributes; + + switch (attribute.AttributeControlType) + { + case AttributeControlType.DropdownList: + case AttributeControlType.RadioList: + case AttributeControlType.ColorSquares: + case AttributeControlType.ImageSquares: + ctrlAttributes = form[controlId]; + if (!string.IsNullOrEmpty(ctrlAttributes)) + { + var selectedAttributeId = int.Parse(ctrlAttributes); + if (selectedAttributeId > 0) + attributesXml = _productAttributeParser.AddProductAttribute(attributesXml, + attribute, selectedAttributeId.ToString()); + } + + break; + case AttributeControlType.Checkboxes: + var cblAttributes = form[controlId].ToString(); + if (!string.IsNullOrEmpty(cblAttributes)) + { + foreach (var item in cblAttributes.Split(_separator, + StringSplitOptions.RemoveEmptyEntries)) + { + var selectedAttributeId = int.Parse(item); + if (selectedAttributeId > 0) + attributesXml = _productAttributeParser.AddProductAttribute(attributesXml, + attribute, selectedAttributeId.ToString()); + } + } + + break; + case AttributeControlType.ReadonlyCheckboxes: + //load read-only (already server-side selected) values + var attributeValues = await _productAttributeService.GetProductAttributeValuesAsync(attribute.Id); + foreach (var selectedAttributeId in attributeValues + .Where(v => v.IsPreSelected) + .Select(v => v.Id) + .ToList()) + { + attributesXml = _productAttributeParser.AddProductAttribute(attributesXml, + attribute, selectedAttributeId.ToString()); + } + + break; + case AttributeControlType.TextBox: + case AttributeControlType.MultilineTextbox: + ctrlAttributes = form[controlId]; + if (!string.IsNullOrEmpty(ctrlAttributes)) + { + var enteredText = ctrlAttributes.ToString().Trim(); + attributesXml = _productAttributeParser.AddProductAttribute(attributesXml, + attribute, enteredText); + } + + break; + case AttributeControlType.Datepicker: + var date = form[controlId + "_day"]; + var month = form[controlId + "_month"]; + var year = form[controlId + "_year"]; + DateTime? selectedDate = null; + try + { + selectedDate = new DateTime(int.Parse(year), int.Parse(month), int.Parse(date)); + } + catch + { + //ignore any exception + } + + if (selectedDate.HasValue) + { + attributesXml = _productAttributeParser.AddProductAttribute(attributesXml, + attribute, selectedDate.Value.ToString("D")); + } + + break; + case AttributeControlType.FileUpload: + var requestForm = await Request.ReadFormAsync(); + var httpPostedFile = requestForm.Files[controlId]; + if (!string.IsNullOrEmpty(httpPostedFile?.FileName)) + { + var fileSizeOk = true; + if (attribute.ValidationFileMaximumSize.HasValue) + { + //compare in bytes + var maxFileSizeBytes = attribute.ValidationFileMaximumSize.Value * 1024; + if (httpPostedFile.Length > maxFileSizeBytes) + { + warnings.Add(string.Format( + await _localizationService.GetResourceAsync("ShoppingCart.MaximumUploadedFileSize"), + attribute.ValidationFileMaximumSize.Value)); + fileSizeOk = false; + } + } + + if (fileSizeOk) + { + //save an uploaded file + var download = new Download + { + DownloadGuid = Guid.NewGuid(), + UseDownloadUrl = false, + DownloadUrl = string.Empty, + DownloadBinary = await _downloadService.GetDownloadBitsAsync(httpPostedFile), + ContentType = httpPostedFile.ContentType, + Filename = _fileProvider.GetFileNameWithoutExtension(httpPostedFile.FileName), + Extension = _fileProvider.GetFileExtension(httpPostedFile.FileName), + IsNew = true + }; + await _downloadService.InsertDownloadAsync(download); + + //save attribute + attributesXml = _productAttributeParser.AddProductAttribute(attributesXml, + attribute, download.DownloadGuid.ToString()); + } + } + + break; + default: + break; + } + } + + //validate conditional attributes (if specified) + foreach (var attribute in attributes) + { + var conditionMet = await _productAttributeParser.IsConditionMetAsync(attribute, attributesXml); + if (conditionMet.HasValue && !conditionMet.Value) + { + attributesXml = _productAttributeParser.RemoveProductAttribute(attributesXml, attribute); + } + } + + return attributesXml; + } + + protected virtual async Task SaveProductWarehouseInventoryAsync(Product product, ProductModel model) + { + ArgumentNullException.ThrowIfNull(product); + + if (model.ManageInventoryMethodId != (int)ManageInventoryMethod.ManageStock) + return; + + if (!model.UseMultipleWarehouses) + return; + + var warehouses = await _shippingService.GetAllWarehousesAsync(); + + var form = await Request.ReadFormAsync(); + var formData = form.ToDictionary(x => x.Key, x => x.Value.ToString()); + + foreach (var warehouse in warehouses) + { + //parse stock quantity + var stockQuantity = 0; + foreach (var formKey in formData.Keys) + { + if (!formKey.Equals($"warehouse_qty_{warehouse.Id}", StringComparison.InvariantCultureIgnoreCase)) + continue; + + _ = int.TryParse(formData[formKey], out stockQuantity); + break; + } + + //parse reserved quantity + var reservedQuantity = 0; + foreach (var formKey in formData.Keys) + if (formKey.Equals($"warehouse_reserved_{warehouse.Id}", StringComparison.InvariantCultureIgnoreCase)) + { + _ = int.TryParse(formData[formKey], out reservedQuantity); + break; + } + + //parse "used" field + var used = false; + foreach (var formKey in formData.Keys) + if (formKey.Equals($"warehouse_used_{warehouse.Id}", StringComparison.InvariantCultureIgnoreCase)) + { + _ = int.TryParse(formData[formKey], out var tmp); + used = tmp == warehouse.Id; + break; + } + + //quantity change history message + var message = $"{await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.MultipleWarehouses")} {await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Edit")}"; + + var existingPwI = (await _productService.GetAllProductWarehouseInventoryRecordsAsync(product.Id)).FirstOrDefault(x => x.WarehouseId == warehouse.Id); + if (existingPwI != null) + { + if (used) + { + var previousStockQuantity = existingPwI.StockQuantity; + + //update existing record + existingPwI.StockQuantity = stockQuantity; + existingPwI.ReservedQuantity = reservedQuantity; + await _productService.UpdateProductWarehouseInventoryAsync(existingPwI); + + //quantity change history + await _productService.AddStockQuantityHistoryEntryAsync(product, existingPwI.StockQuantity - previousStockQuantity, existingPwI.StockQuantity, + existingPwI.WarehouseId, message); + } + else + { + //delete. no need to store record for qty 0 + await _productService.DeleteProductWarehouseInventoryAsync(existingPwI); + + //quantity change history + await _productService.AddStockQuantityHistoryEntryAsync(product, -existingPwI.StockQuantity, 0, existingPwI.WarehouseId, message); + } + } + else + { + if (!used) + continue; + + //no need to insert a record for qty 0 + existingPwI = new ProductWarehouseInventory + { + WarehouseId = warehouse.Id, + ProductId = product.Id, + StockQuantity = stockQuantity, + ReservedQuantity = reservedQuantity + }; + + await _productService.InsertProductWarehouseInventoryAsync(existingPwI); + + //quantity change history + await _productService.AddStockQuantityHistoryEntryAsync(product, existingPwI.StockQuantity, existingPwI.StockQuantity, + existingPwI.WarehouseId, message); + } + } + } + + protected virtual async Task SaveConditionAttributesAsync(ProductAttributeMapping productAttributeMapping, + ProductAttributeConditionModel model, IFormCollection form) + { + string attributesXml = null; + if (model.EnableCondition) + { + var attribute = await _productAttributeService.GetProductAttributeMappingByIdAsync(model.SelectedProductAttributeId); + if (attribute != null) + { + var controlId = $"{NopCatalogDefaults.ProductAttributePrefix}{attribute.Id}"; + switch (attribute.AttributeControlType) + { + case AttributeControlType.DropdownList: + case AttributeControlType.RadioList: + case AttributeControlType.ColorSquares: + case AttributeControlType.ImageSquares: + var ctrlAttributes = form[controlId]; + if (!StringValues.IsNullOrEmpty(ctrlAttributes)) + { + var selectedAttributeId = int.Parse(ctrlAttributes); + //for conditions we should empty values save even when nothing is selected + //otherwise "attributesXml" will be empty + //hence we won't be able to find a selected attribute + attributesXml = _productAttributeParser.AddProductAttribute(null, attribute, + selectedAttributeId > 0 ? selectedAttributeId.ToString() : string.Empty); + } + else + { + //for conditions we should empty values save even when nothing is selected + //otherwise "attributesXml" will be empty + //hence we won't be able to find a selected attribute + attributesXml = _productAttributeParser.AddProductAttribute(null, + attribute, string.Empty); + } + + break; + case AttributeControlType.Checkboxes: + var cblAttributes = form[controlId]; + if (!StringValues.IsNullOrEmpty(cblAttributes)) + { + var anyValueSelected = false; + foreach (var item in cblAttributes.ToString() + .Split(_separator, StringSplitOptions.RemoveEmptyEntries)) + { + var selectedAttributeId = int.Parse(item); + if (selectedAttributeId <= 0) + continue; + + attributesXml = _productAttributeParser.AddProductAttribute(attributesXml, + attribute, selectedAttributeId.ToString()); + anyValueSelected = true; + } + + if (!anyValueSelected) + { + //for conditions we should save empty values even when nothing is selected + //otherwise "attributesXml" will be empty + //hence we won't be able to find a selected attribute + attributesXml = _productAttributeParser.AddProductAttribute(null, + attribute, string.Empty); + } + } + else + { + //for conditions we should save empty values even when nothing is selected + //otherwise "attributesXml" will be empty + //hence we won't be able to find a selected attribute + attributesXml = _productAttributeParser.AddProductAttribute(null, + attribute, string.Empty); + } + + break; + case AttributeControlType.ReadonlyCheckboxes: + case AttributeControlType.TextBox: + case AttributeControlType.MultilineTextbox: + case AttributeControlType.Datepicker: + case AttributeControlType.FileUpload: + default: + //these attribute types are supported as conditions + break; + } + } + } + + productAttributeMapping.ConditionAttributeXml = attributesXml; + await _productAttributeService.UpdateProductAttributeMappingAsync(productAttributeMapping); + } + + protected virtual async Task GenerateAttributeCombinationsAsync(Product product, IList allowedAttributeIds = null) + { + var allAttributesXml = await _productAttributeParser.GenerateAllCombinationsAsync(product, true, allowedAttributeIds); + foreach (var attributesXml in allAttributesXml) + { + var existingCombination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml); + + //already exists? + if (existingCombination != null) + continue; + + //new one + var warnings = new List(); + warnings.AddRange(await _shoppingCartService.GetShoppingCartItemAttributeWarningsAsync(await _workContext.GetCurrentCustomerAsync(), + ShoppingCartType.ShoppingCart, product, 1, attributesXml, true, true, true)); + if (warnings.Any()) + continue; + + //save combination + var combination = new ProductAttributeCombination + { + ProductId = product.Id, + AttributesXml = attributesXml, + StockQuantity = 0, + AllowOutOfStockOrders = false, + Sku = null, + ManufacturerPartNumber = null, + Gtin = null, + OverriddenPrice = null, + NotifyAdminForQuantityBelow = 1 + }; + await _productAttributeService.InsertProductAttributeCombinationAsync(combination); + } + } + + protected virtual async Task PingVideoUrlAsync(string videoUrl) + { + var path = videoUrl.StartsWith('/') + ? $"{_webHelper.GetStoreLocation()}{videoUrl.TrimStart('/')}" + : videoUrl; + + var client = _httpClientFactory.CreateClient(NopHttpDefaults.DefaultHttpClient); + await client.GetStringAsync(path); + } + + protected virtual async Task SaveAttributeCombinationPicturesAsync(Product product, ProductAttributeCombination combination, ProductAttributeCombinationModel model) + { + var existingCombinationPictures = await _productAttributeService.GetProductAttributeCombinationPicturesAsync(combination.Id); + var productPictureIds = (await _pictureService.GetPicturesByProductIdAsync(product.Id)).Select(p => p.Id).ToList(); + + //delete manufacturers + foreach (var existingCombinationPicture in existingCombinationPictures) + if (!model.PictureIds.Contains(existingCombinationPicture.PictureId) || !productPictureIds.Contains(existingCombinationPicture.PictureId)) + await _productAttributeService.DeleteProductAttributeCombinationPictureAsync(existingCombinationPicture); + + //add manufacturers + foreach (var pictureId in model.PictureIds) + { + if (!productPictureIds.Contains(pictureId)) + continue; + + if (_productAttributeService.FindProductAttributeCombinationPicture(existingCombinationPictures, combination.Id, pictureId) == null) + { + await _productAttributeService.InsertProductAttributeCombinationPictureAsync(new ProductAttributeCombinationPicture + { + ProductAttributeCombinationId = combination.Id, + PictureId = pictureId + }); + } + } + } + + protected virtual async Task SaveAttributeValuePicturesAsync(Product product, ProductAttributeValue value, ProductAttributeValueModel model) + { + var existingValuePictures = await _productAttributeService.GetProductAttributeValuePicturesAsync(value.Id); + var productPictureIds = (await _pictureService.GetPicturesByProductIdAsync(product.Id)).Select(p => p.Id).ToList(); + + //delete manufacturers + foreach (var existingValuePicture in existingValuePictures) + if (!model.PictureIds.Contains(existingValuePicture.PictureId) || !productPictureIds.Contains(existingValuePicture.PictureId)) + await _productAttributeService.DeleteProductAttributeValuePictureAsync(existingValuePicture); + + //add manufacturers + foreach (var pictureId in model.PictureIds) + { + if (!productPictureIds.Contains(pictureId)) + continue; + + if (_productAttributeService.FindProductAttributeValuePicture(existingValuePictures, value.Id, pictureId) == null) + { + await _productAttributeService.InsertProductAttributeValuePictureAsync(new ProductAttributeValuePicture + { + ProductAttributeValueId = value.Id, + PictureId = pictureId + }); + } + } + } + + protected virtual async Task> ParseBulkEditDataAsync() + { + var rez = new Dictionary(); + var currentVendor = await _workContext.GetCurrentVendorAsync(); + + foreach (var item in Request.Form) + { + if (getData(item, "product-select-", out var productId)) + setData(productId, data => + { + data.IsSelected = true; + }); + + if (getData(item, "name-", out productId)) + setData(productId, data => + { + data.Name = item.Value; + }); + + if (getData(item, "sku-", out productId)) + setData(productId, data => + { + data.Sku = item.Value; + }); + + if (getData(item, "price-", out productId)) + setData(productId, data => + { + data.Price = decimal.Parse(item.Value); + }); + + if (getData(item, "old-price-", out productId)) + setData(productId, data => + { + data.OldPrice = decimal.Parse(item.Value); + }); + + if (getData(item, "quantity-", out productId)) + setData(productId, data => + { + data.Quantity = int.Parse(item.Value); + }); + + if (getData(item, "published-", out productId)) + setData(productId, data => + { + data.IsPublished = true; + }); + } + + var productIds = rez.Select(p => p.Key).ToArray(); + + var products = await _productService.GetProductsByIdsAsync(productIds); + + foreach (var product in products) + rez[product.Id].Product = product; + + return rez.Values.ToList(); + + bool getData(KeyValuePair item, string selector, out int productId) + { + var key = item.Key; + productId = 0; + + if (!key.StartsWith(selector)) + return false; + + productId = int.Parse(key.Replace(selector, string.Empty)); + + return true; + } + + void setData(int productId, Action action) + { + if (!rez.ContainsKey(productId)) + rez.Add(productId, new BulkEditData(_taxSettings.DefaultTaxCategoryId, currentVendor?.Id ?? 0)); + + action(rez[productId]); + } + } + + #endregion + + #region Methods + + #region Product list / create / edit / delete + + public virtual IActionResult Index() + { + return RedirectToAction("List"); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task List() + { + //prepare model + var model = await _productModelFactory.PrepareProductSearchModelAsync(new ProductSearchModel()); + + return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Product/List.cshtml", model); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task BulkEdit() + { + //prepare model + var model = await _productModelFactory.PrepareProductSearchModelAsync(new ProductSearchModel()); + model.Length = _adminAreaSettings.ProductsBulkEditGridPageSize; + + return View(model); + } + + [HttpPost, ActionName("BulkEdit"), ParameterBasedOnFormName("bulk-edit-save-selected", "selected")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public async Task BulkEditSave(ProductSearchModel searchModel, bool selected) + { + var data = await ParseBulkEditDataAsync(); + + var productsToUpdate = data.Where(d => d.NeedToUpdate(selected)).ToList(); + await _productService.UpdateProductsAsync(productsToUpdate.Select(d => d.UpdateProduct(selected)).ToList()); + + var productsToInsert = data.Where(d => d.NeedToCreate(selected)).ToList(); + await _productService.InsertProductsAsync(productsToInsert.Select(d => d.CreateProduct(selected)).ToList()); + + //prepare model + var model = await _productModelFactory.PrepareProductSearchModelAsync(searchModel); + model.Length = _adminAreaSettings.ProductsBulkEditGridPageSize; + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductList(ProductSearchModel searchModel) + { + //prepare model + var model = await _productModelFactory.PrepareProductListModelAsync(searchModel); + + return Json(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task BulkEditProducts(ProductSearchModel searchModel) + { + //prepare model + var model = await _productModelFactory.PrepareProductListModelAsync(searchModel); + var html = await RenderPartialViewToStringAsync("_BulkEdit.Products", model.Data.ToList()); + + return Json(new Dictionary { { "Html", html }, { "Products", model } }); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task BulkEditNewProduct(int id) + { + var primaryStoreCurrencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId)).CurrencyCode; + + //prepare model + var model = new List { new() + { + Id = id, + PrimaryStoreCurrencyCode = primaryStoreCurrencyCode, + Published = true + } }; + + var html = await RenderPartialViewToStringAsync("_BulkEdit.Products", model); + + return Json(html); + } + + [HttpPost, ActionName("List")] + [FormValueRequired("go-to-product-by-sku")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task GoToSku(ProductSearchModel searchModel) + { + //try to load a product entity, if not found, then try to load a product attribute combination + var productId = (await _productService.GetProductBySkuAsync(searchModel.GoDirectlyToSku))?.Id + ?? (await _productAttributeService.GetProductAttributeCombinationBySkuAsync(searchModel.GoDirectlyToSku))?.ProductId; + + if (productId != null) + return RedirectToAction("Edit", "Product", new { id = productId }); + + //not found + return await List(); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task Create(bool showtour = false) + { + //validate maximum number of products per vendor + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (_vendorSettings.MaximumProductNumber > 0 && + currentVendor != null && + await _productService.GetNumberOfProductsByVendorIdAsync(currentVendor.Id) >= _vendorSettings.MaximumProductNumber) + { + _notificationService.ErrorNotification(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ExceededMaximumNumber"), + _vendorSettings.MaximumProductNumber)); + return RedirectToAction("List"); + } + + //prepare model + var model = await _productModelFactory.PrepareProductModelAsync(new ProductModel(), null); + + //show configuration tour + if (showtour) + { + var customer = await _workContext.GetCurrentCustomerAsync(); + var hideCard = await _genericAttributeService.GetAttributeAsync(customer, NopCustomerDefaults.HideConfigurationStepsAttribute); + var closeCard = await _genericAttributeService.GetAttributeAsync(customer, NopCustomerDefaults.CloseConfigurationStepsAttribute); + + if (!hideCard && !closeCard) + ViewBag.ShowTour = true; + } + + return View(model); + } + + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task Create(ProductModel model, bool continueEditing) + { + //validate maximum number of products per vendor + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (_vendorSettings.MaximumProductNumber > 0 && + currentVendor != null && + await _productService.GetNumberOfProductsByVendorIdAsync(currentVendor.Id) >= _vendorSettings.MaximumProductNumber) + { + _notificationService.ErrorNotification(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ExceededMaximumNumber"), + _vendorSettings.MaximumProductNumber)); + return RedirectToAction("List"); + } + + if (ModelState.IsValid) + { + //a vendor should have access only to his products + if (currentVendor != null) + model.VendorId = currentVendor.Id; + + //vendors cannot edit "Show on home page" property + if (currentVendor != null && model.ShowOnHomepage) + model.ShowOnHomepage = false; + + //product + var product = model.ToEntity(); + product.CreatedOnUtc = DateTime.UtcNow; + product.UpdatedOnUtc = DateTime.UtcNow; + await _productService.InsertProductAsync(product); + + //search engine name + model.SeName = await _urlRecordService.ValidateSeNameAsync(product, model.SeName, product.Name, true); + await _urlRecordService.SaveSlugAsync(product, model.SeName, 0); + + //locales + await UpdateLocalesAsync(product, model); + + //categories + await SaveCategoryMappingsAsync(product, model); + + //manufacturers + await SaveManufacturerMappingsAsync(product, model); + + //stores + await _productService.UpdateProductStoreMappingsAsync(product, model.SelectedStoreIds); + + //discounts + await SaveDiscountMappingsAsync(product, model); + + //tags + await _productTagService.UpdateProductTagsAsync(product, model.SelectedProductTags.ToArray()); + + //warehouses + await SaveProductWarehouseInventoryAsync(product, model); + + //quantity change history + await _productService.AddStockQuantityHistoryEntryAsync(product, product.StockQuantity, product.StockQuantity, product.WarehouseId, + await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Edit")); + + //activity log + await _customerActivityService.InsertActivityAsync("AddNewProduct", + string.Format(await _localizationService.GetResourceAsync("ActivityLog.AddNewProduct"), product.Name), product); + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Added")); + + if (!continueEditing) + return RedirectToAction("List"); + + return RedirectToAction("Edit", new { id = product.Id }); + } + + //prepare model + model = await _productModelFactory.PrepareProductModelAsync(model, null, true); + + //if we got this far, something failed, redisplay form + return View(model); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task Edit(int id) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(id); + if (product == null || product.Deleted) + return RedirectToAction("List"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List"); + + //prepare model + var model = await _productModelFactory.PrepareProductModelAsync(null, product); + + return View(model); + } + + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task Edit(ProductModel model, bool continueEditing) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(model.Id); + if (product == null || product.Deleted) + return RedirectToAction("List"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List"); + + //check if the product quantity has been changed while we were editing the product + //and if it has been changed then we show error notification + //and redirect on the editing page without data saving + if (product.StockQuantity != model.LastStockQuantity) + { + _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.StockQuantity.ChangedWarning")); + return RedirectToAction("Edit", new { id = product.Id }); + } + + if (ModelState.IsValid) + { + //a vendor should have access only to his products + if (currentVendor != null) + model.VendorId = currentVendor.Id; + + //we do not validate maximum number of products per vendor when editing existing products (only during creation of new products) + //vendors cannot edit "Show on home page" property + if (currentVendor != null && model.ShowOnHomepage != product.ShowOnHomepage) + model.ShowOnHomepage = product.ShowOnHomepage; + + //some previously used values + var prevTotalStockQuantity = await _productService.GetTotalStockQuantityAsync(product); + var prevDownloadId = product.DownloadId; + var prevSampleDownloadId = product.SampleDownloadId; + var previousStockQuantity = product.StockQuantity; + var previousWarehouseId = product.WarehouseId; + var previousProductType = product.ProductType; + + //product + product = model.ToEntity(product); + + product.UpdatedOnUtc = DateTime.UtcNow; + await _productService.UpdateProductAsync(product); + + //remove associated products + if (previousProductType == ProductType.GroupedProduct && product.ProductType == ProductType.SimpleProduct) + { + var store = await _storeContext.GetCurrentStoreAsync(); + var storeId = store?.Id ?? 0; + var vendorId = currentVendor?.Id ?? 0; + + var associatedProducts = await _productService.GetAssociatedProductsAsync(product.Id, storeId, vendorId); + foreach (var associatedProduct in associatedProducts) + { + associatedProduct.ParentGroupedProductId = 0; + await _productService.UpdateProductAsync(associatedProduct); + } + } + + //search engine name + model.SeName = await _urlRecordService.ValidateSeNameAsync(product, model.SeName, product.Name, true); + await _urlRecordService.SaveSlugAsync(product, model.SeName, 0); + + //locales + await UpdateLocalesAsync(product, model); + + //tags + await _productTagService.UpdateProductTagsAsync(product, model.SelectedProductTags.ToArray()); + + //warehouses + await SaveProductWarehouseInventoryAsync(product, model); + + //categories + await SaveCategoryMappingsAsync(product, model); + + //manufacturers + await SaveManufacturerMappingsAsync(product, model); + + //stores + await _productService.UpdateProductStoreMappingsAsync(product, model.SelectedStoreIds); + + //discounts + await SaveDiscountMappingsAsync(product, model); + + //picture seo names + await UpdatePictureSeoNamesAsync(product); + + //back in stock notifications + if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock && + product.BackorderMode == BackorderMode.NoBackorders && + product.AllowBackInStockSubscriptions && + await _productService.GetTotalStockQuantityAsync(product) > 0 && + prevTotalStockQuantity <= 0 && + product.Published && + !product.Deleted) + { + await _backInStockSubscriptionService.SendNotificationsToSubscribersAsync(product); + } + + //delete an old "download" file (if deleted or updated) + if (prevDownloadId > 0 && prevDownloadId != product.DownloadId) + { + var prevDownload = await _downloadService.GetDownloadByIdAsync(prevDownloadId); + if (prevDownload != null) + await _downloadService.DeleteDownloadAsync(prevDownload); + } + + //delete an old "sample download" file (if deleted or updated) + if (prevSampleDownloadId > 0 && prevSampleDownloadId != product.SampleDownloadId) + { + var prevSampleDownload = await _downloadService.GetDownloadByIdAsync(prevSampleDownloadId); + if (prevSampleDownload != null) + await _downloadService.DeleteDownloadAsync(prevSampleDownload); + } + + //quantity change history + if (previousWarehouseId != product.WarehouseId) + { + //warehouse is changed + //compose a message + var oldWarehouseMessage = string.Empty; + if (previousWarehouseId > 0) + { + var oldWarehouse = await _shippingService.GetWarehouseByIdAsync(previousWarehouseId); + if (oldWarehouse != null) + oldWarehouseMessage = string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.EditWarehouse.Old"), oldWarehouse.Name); + } + + var newWarehouseMessage = string.Empty; + if (product.WarehouseId > 0) + { + var newWarehouse = await _shippingService.GetWarehouseByIdAsync(product.WarehouseId); + if (newWarehouse != null) + newWarehouseMessage = string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.EditWarehouse.New"), newWarehouse.Name); + } + + var message = string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.EditWarehouse"), oldWarehouseMessage, newWarehouseMessage); + + //record history + await _productService.AddStockQuantityHistoryEntryAsync(product, -previousStockQuantity, 0, previousWarehouseId, message); + await _productService.AddStockQuantityHistoryEntryAsync(product, product.StockQuantity, product.StockQuantity, product.WarehouseId, message); + } + else + { + await _productService.AddStockQuantityHistoryEntryAsync(product, product.StockQuantity - previousStockQuantity, product.StockQuantity, + product.WarehouseId, await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Edit")); + } + + //activity log + await _customerActivityService.InsertActivityAsync("EditProduct", + string.Format(await _localizationService.GetResourceAsync("ActivityLog.EditProduct"), product.Name), product); + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Updated")); + + if (!continueEditing) + return RedirectToAction("List"); + + return RedirectToAction("Edit", new { id = product.Id }); + } + + //prepare model + model = await _productModelFactory.PrepareProductModelAsync(model, product, true); + + //if we got this far, something failed, redisplay form + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task Delete(int id) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(id); + if (product == null) + return RedirectToAction("List"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List"); + + await _productService.DeleteProductAsync(product); + + //activity log + await _customerActivityService.InsertActivityAsync("DeleteProduct", + string.Format(await _localizationService.GetResourceAsync("ActivityLog.DeleteProduct"), product.Name), product); + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Deleted")); + + return RedirectToAction("List"); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task DeleteSelected(ICollection selectedIds) + { + if (selectedIds == null || !selectedIds.Any()) + return NoContent(); + + var currentVendor = await _workContext.GetCurrentVendorAsync(); + + var products = (await _productService.GetProductsByIdsAsync(selectedIds.ToArray())) + .Where(p => currentVendor == null || p.VendorId == currentVendor.Id).ToList(); + + await _productService.DeleteProductsAsync(products); + + //activity log + var activityLogFormat = await _localizationService.GetResourceAsync("ActivityLog.DeleteProduct"); + + foreach (var product in products) + await _customerActivityService.InsertActivityAsync("DeleteProduct", + string.Format(activityLogFormat, product.Name), product); + + return Json(new { Result = true }); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task CopyProduct(ProductModel model) + { + var copyModel = model.CopyProductModel; + try + { + var originalProduct = await _productService.GetProductByIdAsync(copyModel.Id); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && originalProduct.VendorId != currentVendor.Id) + return RedirectToAction("List"); + + var newProduct = await _copyProductService.CopyProductAsync(originalProduct, copyModel.Name, copyModel.Published, copyModel.CopyMultimedia); + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Copied")); + + return RedirectToAction("Edit", new { id = newProduct.Id }); + } + catch (Exception exc) + { + _notificationService.ErrorNotification(exc.Message); + return RedirectToAction("Edit", new { id = copyModel.Id }); + } + } + + //action displaying notification (warning) to a store owner that entered SKU already exists + public virtual async Task SkuReservedWarning(int productId, string sku) + { + string message; + + //check whether product with passed SKU already exists + var productBySku = await _productService.GetProductBySkuAsync(sku); + if (productBySku != null) + { + if (productBySku.Id == productId) + return Json(new { Result = string.Empty }); + + message = string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.Sku.Reserved"), productBySku.Name); + return Json(new { Result = message }); + } + + //check whether combination with passed SKU already exists + var combinationBySku = await _productAttributeService.GetProductAttributeCombinationBySkuAsync(sku); + if (combinationBySku == null) + return Json(new { Result = string.Empty }); + + message = string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.Fields.Sku.Reserved"), + (await _productService.GetProductByIdAsync(combinationBySku.ProductId))?.Name); + + return Json(new { Result = message }); + } + + #endregion + + #region Required products + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task LoadProductFriendlyNames(string productIds) + { + var result = string.Empty; + + if (string.IsNullOrWhiteSpace(productIds)) + return Json(new { Text = result }); + + var ids = new List(); + var rangeArray = productIds + .Split(_separator, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .ToList(); + + foreach (var str1 in rangeArray) + { + if (int.TryParse(str1, out var tmp1)) + ids.Add(tmp1); + } + + var products = await _productService.GetProductsByIdsAsync(ids.ToArray()); + for (var i = 0; i <= products.Count - 1; i++) + { + result += products[i].Name; + if (i != products.Count - 1) + result += ", "; + } + + return Json(new { Text = result }); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task RequiredProductAddPopup() + { + //prepare model + var model = await _productModelFactory.PrepareAddRequiredProductSearchModelAsync(new AddRequiredProductSearchModel()); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task RequiredProductAddPopupList(AddRequiredProductSearchModel searchModel) + { + //prepare model + var model = await _productModelFactory.PrepareAddRequiredProductListModelAsync(searchModel); + + return Json(model); + } + + #endregion + + #region Related products + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task RelatedProductList(RelatedProductSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareRelatedProductListModelAsync(searchModel, product); + + return Json(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task RelatedProductUpdate(RelatedProductModel model) + { + //try to get a related product with the specified id + var relatedProduct = await _productService.GetRelatedProductByIdAsync(model.Id) + ?? throw new ArgumentException("No related product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + var product = await _productService.GetProductByIdAsync(relatedProduct.ProductId1); + if (product != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + } + + relatedProduct.DisplayOrder = model.DisplayOrder; + await _productService.UpdateRelatedProductAsync(relatedProduct); + + return new NullJsonResult(); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task RelatedProductDelete(int id) + { + //try to get a related product with the specified id + var relatedProduct = await _productService.GetRelatedProductByIdAsync(id) + ?? throw new ArgumentException("No related product found with the specified id"); + + var productId = relatedProduct.ProductId1; + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + var product = await _productService.GetProductByIdAsync(productId); + if (product != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + } + + await _productService.DeleteRelatedProductAsync(relatedProduct); + + return new NullJsonResult(); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task RelatedProductAddPopup(int productId) + { + //prepare model + var model = await _productModelFactory.PrepareAddRelatedProductSearchModelAsync(new AddRelatedProductSearchModel()); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task RelatedProductAddPopupList(AddRelatedProductSearchModel searchModel) + { + //prepare model + var model = await _productModelFactory.PrepareAddRelatedProductListModelAsync(searchModel); + + return Json(model); + } + + [HttpPost] + [FormValueRequired("save")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task RelatedProductAddPopup(AddRelatedProductModel model) + { + var selectedProducts = await _productService.GetProductsByIdsAsync(model.SelectedProductIds.ToArray()); + if (selectedProducts.Any()) + { + var existingRelatedProducts = await _productService.GetRelatedProductsByProductId1Async(model.ProductId, showHidden: true); + var currentVendor = await _workContext.GetCurrentVendorAsync(); + foreach (var product in selectedProducts) + { + //a vendor should have access only to his products + if (currentVendor != null && product.VendorId != currentVendor.Id) + continue; + + if (_productService.FindRelatedProduct(existingRelatedProducts, model.ProductId, product.Id) != null) + continue; + + await _productService.InsertRelatedProductAsync(new RelatedProduct + { + ProductId1 = model.ProductId, + ProductId2 = product.Id, + DisplayOrder = 1 + }); + } + } + + ViewBag.RefreshPage = true; + + return View(new AddRelatedProductSearchModel()); + } + + #endregion + + #region Cross-sell products + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task CrossSellProductList(CrossSellProductSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareCrossSellProductListModelAsync(searchModel, product); + + return Json(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task CrossSellProductDelete(int id) + { + //try to get a cross-sell product with the specified id + var crossSellProduct = await _productService.GetCrossSellProductByIdAsync(id) + ?? throw new ArgumentException("No cross-sell product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + var product = await _productService.GetProductByIdAsync(crossSellProduct.ProductId1); + if (product != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + } + + await _productService.DeleteCrossSellProductAsync(crossSellProduct); + + return new NullJsonResult(); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task CrossSellProductAddPopup(int productId) + { + //prepare model + var model = await _productModelFactory.PrepareAddCrossSellProductSearchModelAsync(new AddCrossSellProductSearchModel()); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task CrossSellProductAddPopupList(AddCrossSellProductSearchModel searchModel) + { + //prepare model + var model = await _productModelFactory.PrepareAddCrossSellProductListModelAsync(searchModel); + + return Json(model); + } + + [HttpPost] + [FormValueRequired("save")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task CrossSellProductAddPopup(AddCrossSellProductModel model) + { + var selectedProducts = await _productService.GetProductsByIdsAsync(model.SelectedProductIds.ToArray()); + if (selectedProducts.Any()) + { + var existingCrossSellProducts = await _productService.GetCrossSellProductsByProductId1Async(model.ProductId, showHidden: true); + var currentVendor = await _workContext.GetCurrentVendorAsync(); + foreach (var product in selectedProducts) + { + //a vendor should have access only to his products + if (currentVendor != null && product.VendorId != currentVendor.Id) + continue; + + if (_productService.FindCrossSellProduct(existingCrossSellProducts, model.ProductId, product.Id) != null) + continue; + + await _productService.InsertCrossSellProductAsync(new CrossSellProduct + { + ProductId1 = model.ProductId, + ProductId2 = product.Id + }); + } + } + + ViewBag.RefreshPage = true; + + return View(new AddCrossSellProductSearchModel()); + } + + #endregion + + #region Associated products + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task AssociatedProductList(AssociatedProductSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareAssociatedProductListModelAsync(searchModel, product); + + return Json(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task AssociatedProductUpdate(AssociatedProductModel model) + { + //try to get an associated product with the specified id + var associatedProduct = await _productService.GetProductByIdAsync(model.Id) + ?? throw new ArgumentException("No associated product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && associatedProduct.VendorId != currentVendor.Id) + return Content("This is not your product"); + + associatedProduct.DisplayOrder = model.DisplayOrder; + await _productService.UpdateProductAsync(associatedProduct); + + return new NullJsonResult(); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task AssociatedProductDelete(int id) + { + //try to get an associated product with the specified id + var product = await _productService.GetProductByIdAsync(id) + ?? throw new ArgumentException("No associated product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + product.ParentGroupedProductId = 0; + await _productService.UpdateProductAsync(product); + + return new NullJsonResult(); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task AssociatedProductAddPopup(int productId) + { + //prepare model + var model = await _productModelFactory.PrepareAddAssociatedProductSearchModelAsync(new AddAssociatedProductSearchModel()); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task AssociatedProductAddPopupList(AddAssociatedProductSearchModel searchModel) + { + //prepare model + var model = await _productModelFactory.PrepareAddAssociatedProductListModelAsync(searchModel); + + return Json(model); + } + + [HttpPost] + [FormValueRequired("save")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task AssociatedProductAddPopup(AddAssociatedProductModel model) + { + var selectedProducts = await _productService.GetProductsByIdsAsync(model.SelectedProductIds.ToArray()); + + var tryToAddSelfGroupedProduct = selectedProducts + .Select(p => p.Id) + .Contains(model.ProductId); + + if (selectedProducts.Any()) + { + foreach (var product in selectedProducts) + { + if (product.Id == model.ProductId) + continue; + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + continue; + + product.ParentGroupedProductId = model.ProductId; + await _productService.UpdateProductAsync(product); + } + } + + if (tryToAddSelfGroupedProduct) + { + _notificationService.WarningNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.AssociatedProducts.TryToAddSelfGroupedProduct")); + + var addAssociatedProductSearchModel = await _productModelFactory.PrepareAddAssociatedProductSearchModelAsync(new AddAssociatedProductSearchModel()); + //set current product id + addAssociatedProductSearchModel.ProductId = model.ProductId; + + ViewBag.RefreshPage = true; + + return View(addAssociatedProductSearchModel); + } + + ViewBag.RefreshPage = true; + + ViewBag.ClosePage = true; + + return View(new AddAssociatedProductSearchModel()); + } + + #endregion + + #region Product pictures + + [HttpPost] + [IgnoreAntiforgeryToken] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductPictureAdd(int productId, IFormCollection form) + { + if (productId == 0) + throw new ArgumentException(); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productId) + ?? throw new ArgumentException("No product found with the specified id"); + + var files = form.Files.ToList(); + if (!files.Any()) + return Json(new { success = false }); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List"); + try + { + foreach (var file in files) + { + //insert picture + var picture = await _pictureService.InsertPictureAsync(file); + + await _pictureService.SetSeoFilenameAsync(picture.Id, await _pictureService.GetPictureSeNameAsync(product.Name)); + + await _productService.InsertProductPictureAsync(new ProductPicture + { + PictureId = picture.Id, + ProductId = product.Id, + DisplayOrder = 0 + }); + } + } + catch (Exception exc) + { + return Json(new + { + success = false, + message = $"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Pictures.Alert.PictureAdd")} {exc.Message}", + }); + } + + return Json(new { success = true }); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductPictureList(ProductPictureSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareProductPictureListModelAsync(searchModel, product); + + return Json(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductPictureUpdate(ProductPictureModel model) + { + //try to get a product picture with the specified id + var productPicture = await _productService.GetProductPictureByIdAsync(model.Id) + ?? throw new ArgumentException("No product picture found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + var product = await _productService.GetProductByIdAsync(productPicture.ProductId); + if (product != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + } + + //try to get a picture with the specified id + var picture = await _pictureService.GetPictureByIdAsync(productPicture.PictureId) + ?? throw new ArgumentException("No picture found with the specified id"); + + await _pictureService.UpdatePictureAsync(picture.Id, + await _pictureService.LoadPictureBinaryAsync(picture), + picture.MimeType, + picture.SeoFilename, + model.OverrideAltAttribute, + model.OverrideTitleAttribute); + + productPicture.DisplayOrder = model.DisplayOrder; + await _productService.UpdateProductPictureAsync(productPicture); + + return new NullJsonResult(); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductPictureDelete(int id) + { + //try to get a product picture with the specified id + var productPicture = await _productService.GetProductPictureByIdAsync(id) + ?? throw new ArgumentException("No product picture found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + var product = await _productService.GetProductByIdAsync(productPicture.ProductId); + if (product != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + } + + var pictureId = productPicture.PictureId; + await _productService.DeleteProductPictureAsync(productPicture); + + //try to get a picture with the specified id + var picture = await _pictureService.GetPictureByIdAsync(pictureId) + ?? throw new ArgumentException("No picture found with the specified id"); + + await _pictureService.DeletePictureAsync(picture); + + return new NullJsonResult(); + } + + #endregion + + #region Product videos + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductVideoAdd(int productId, [Validate] ProductVideoModel model) + { + if (productId == 0) + throw new ArgumentException(); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productId) + ?? throw new ArgumentException("No product found with the specified id"); + + if (string.IsNullOrEmpty(model.VideoUrl)) + ModelState.AddModelError(string.Empty, + await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Videos.Alert.VideoAdd.EmptyUrl")); + + if (!ModelState.IsValid) + return ErrorJson(ModelState.SerializeErrors()); + + var videoUrl = model.VideoUrl.TrimStart('~'); + + try + { + await PingVideoUrlAsync(videoUrl); + } + catch (Exception exc) + { + return Json(new + { + success = false, + error = $"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Videos.Alert.VideoAdd")} {exc.Message}", + }); + } + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List"); + try + { + var video = new Video + { + VideoUrl = videoUrl + }; + + //insert video + await _videoService.InsertVideoAsync(video); + + await _productService.InsertProductVideoAsync(new ProductVideo + { + VideoId = video.Id, + ProductId = product.Id, + DisplayOrder = model.DisplayOrder + }); + } + catch (Exception exc) + { + return Json(new + { + success = false, + error = $"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Videos.Alert.VideoAdd")} {exc.Message}", + }); + } + + return Json(new { success = true }); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductVideoList(ProductVideoSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareProductVideoListModelAsync(searchModel, product); + + return Json(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductVideoUpdate([Validate] ProductVideoModel model) + { + //try to get a product picture with the specified id + var productVideo = await _productService.GetProductVideoByIdAsync(model.Id) + ?? throw new ArgumentException("No product video found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + var product = await _productService.GetProductByIdAsync(productVideo.ProductId); + if (product != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + } + + //try to get a video with the specified id + var video = await _videoService.GetVideoByIdAsync(productVideo.VideoId) + ?? throw new ArgumentException("No video found with the specified id"); + + var videoUrl = model.VideoUrl.TrimStart('~'); + + try + { + await PingVideoUrlAsync(videoUrl); + } + catch (Exception exc) + { + return Json(new + { + success = false, + error = $"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Videos.Alert.VideoUpdate")} {exc.Message}", + }); + } + + video.VideoUrl = videoUrl; + + await _videoService.UpdateVideoAsync(video); + + productVideo.DisplayOrder = model.DisplayOrder; + await _productService.UpdateProductVideoAsync(productVideo); + + return new NullJsonResult(); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductVideoDelete(int id) + { + //try to get a product video with the specified id + var productVideo = await _productService.GetProductVideoByIdAsync(id) + ?? throw new ArgumentException("No product video found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + var product = await _productService.GetProductByIdAsync(productVideo.ProductId); + if (product != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + } + + var videoId = productVideo.VideoId; + await _productService.DeleteProductVideoAsync(productVideo); + + //try to get a video with the specified id + var video = await _videoService.GetVideoByIdAsync(videoId) + ?? throw new ArgumentException("No video found with the specified id"); + + await _videoService.DeleteVideoAsync(video); + + return new NullJsonResult(); + } + + #endregion + + #region Product specification attributes + + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductSpecificationAttributeAdd(AddSpecificationAttributeModel model, bool continueEditing) + { + var product = await _productService.GetProductByIdAsync(model.ProductId); + if (product == null) + { + _notificationService.ErrorNotification("No product found with the specified id"); + return RedirectToAction("List"); + } + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + { + return RedirectToAction("List"); + } + + //we allow filtering only for "Option" attribute type + if (model.AttributeTypeId != (int)SpecificationAttributeType.Option) + model.AllowFiltering = false; + + //we don't allow CustomValue for "Option" attribute type + if (model.AttributeTypeId == (int)SpecificationAttributeType.Option) + model.ValueRaw = null; + + //store raw html if field allow this + if (model.AttributeTypeId == (int)SpecificationAttributeType.CustomText || model.AttributeTypeId == (int)SpecificationAttributeType.Hyperlink) + model.ValueRaw = model.Value; + + var psa = model.ToEntity(); + psa.CustomValue = model.ValueRaw; + await _specificationAttributeService.InsertProductSpecificationAttributeAsync(psa); + + switch (psa.AttributeType) + { + case SpecificationAttributeType.CustomText: + foreach (var localized in model.Locales) + { + await _localizedEntityService.SaveLocalizedValueAsync(psa, + x => x.CustomValue, + localized.Value, + localized.LanguageId); + } + + break; + case SpecificationAttributeType.CustomHtmlText: + foreach (var localized in model.Locales) + { + await _localizedEntityService.SaveLocalizedValueAsync(psa, + x => x.CustomValue, + localized.ValueRaw, + localized.LanguageId); + } + + break; + case SpecificationAttributeType.Option: + break; + case SpecificationAttributeType.Hyperlink: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (continueEditing) + return RedirectToAction("ProductSpecAttributeAddOrEdit", + new { productId = psa.ProductId, specificationId = psa.Id }); + + //select an appropriate card + SaveSelectedCardName("product-specification-attributes"); + return RedirectToAction("Edit", new { id = model.ProductId }); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductSpecAttrList(ProductSpecificationAttributeSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareProductSpecificationAttributeListModelAsync(searchModel, product); + + return Json(model); + } + + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductSpecAttrUpdate(AddSpecificationAttributeModel model, bool continueEditing) + { + //try to get a product specification attribute with the specified id + var psa = await _specificationAttributeService.GetProductSpecificationAttributeByIdAsync(model.SpecificationId); + if (psa == null) + { + //select an appropriate card + SaveSelectedCardName("product-specification-attributes"); + _notificationService.ErrorNotification("No product specification attribute found with the specified id"); + + return RedirectToAction("Edit", new { id = model.ProductId }); + } + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null + && (await _productService.GetProductByIdAsync(psa.ProductId)).VendorId != currentVendor.Id) + { + _notificationService.ErrorNotification("This is not your product"); + + return RedirectToAction("List"); + } + + //we allow filtering and change option only for "Option" attribute type + //save localized values for CustomHtmlText and CustomText + switch (model.AttributeTypeId) + { + case (int)SpecificationAttributeType.Option: + psa.AllowFiltering = model.AllowFiltering; + psa.SpecificationAttributeOptionId = model.SpecificationAttributeOptionId; + + break; + case (int)SpecificationAttributeType.CustomHtmlText: + psa.CustomValue = model.ValueRaw; + foreach (var localized in model.Locales) + { + await _localizedEntityService.SaveLocalizedValueAsync(psa, + x => x.CustomValue, + localized.ValueRaw, + localized.LanguageId); + } + + break; + case (int)SpecificationAttributeType.CustomText: + psa.CustomValue = model.Value; + foreach (var localized in model.Locales) + { + await _localizedEntityService.SaveLocalizedValueAsync(psa, + x => x.CustomValue, + localized.Value, + localized.LanguageId); + } + + break; + default: + psa.CustomValue = model.Value; + + break; + } + + psa.ShowOnProductPage = model.ShowOnProductPage; + psa.DisplayOrder = model.DisplayOrder; + await _specificationAttributeService.UpdateProductSpecificationAttributeAsync(psa); + + if (continueEditing) + { + return RedirectToAction("ProductSpecAttributeAddOrEdit", + new { productId = psa.ProductId, specificationId = model.SpecificationId }); + } + + //select an appropriate card + SaveSelectedCardName("product-specification-attributes"); + + return RedirectToAction("Edit", new { id = psa.ProductId }); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductSpecAttributeAddOrEdit(int productId, int? specificationId) + { + if (!specificationId.HasValue && !await _permissionService.AuthorizeAsync(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)) + return AccessDeniedView(); + + if (await _productService.GetProductByIdAsync(productId) == null) + { + _notificationService.ErrorNotification("No product found with the specified id"); + return RedirectToAction("List"); + } + + //try to get a product specification attribute with the specified id + try + { + var model = await _productModelFactory.PrepareAddSpecificationAttributeModelAsync(productId, specificationId); + return View(model); + } + catch (Exception ex) + { + await _notificationService.ErrorNotificationAsync(ex); + + //select an appropriate card + SaveSelectedCardName("product-specification-attributes"); + return RedirectToAction("Edit", new { id = productId }); + } + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductSpecAttrDelete(AddSpecificationAttributeModel model) + { + //try to get a product specification attribute with the specified id + var psa = await _specificationAttributeService.GetProductSpecificationAttributeByIdAsync(model.SpecificationId); + if (psa == null) + { + //select an appropriate card + SaveSelectedCardName("product-specification-attributes"); + _notificationService.ErrorNotification("No product specification attribute found with the specified id"); + return RedirectToAction("Edit", new { id = model.ProductId }); + } + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && (await _productService.GetProductByIdAsync(psa.ProductId)).VendorId != currentVendor.Id) + { + _notificationService.ErrorNotification("This is not your product"); + return RedirectToAction("List", new { id = model.ProductId }); + } + + await _specificationAttributeService.DeleteProductSpecificationAttributeAsync(psa); + + //select an appropriate card + SaveSelectedCardName("product-specification-attributes"); + + return RedirectToAction("Edit", new { id = psa.ProductId }); + } + + #endregion + + #region Product tags + + [CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_VIEW)] + public virtual async Task ProductTags() + { + //prepare model + var model = await _productModelFactory.PrepareProductTagSearchModelAsync(new ProductTagSearchModel()); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_VIEW)] + public virtual async Task ProductTags(ProductTagSearchModel searchModel) + { + //prepare model + var model = await _productModelFactory.PrepareProductTagListModelAsync(searchModel); + + return Json(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_CREATE_EDIT_DELETE)] + public virtual async Task ProductTagDelete(int id) + { + //try to get a product tag with the specified id + var tag = await _productTagService.GetProductTagByIdAsync(id) + ?? throw new ArgumentException("No product tag found with the specified id"); + + await _productTagService.DeleteProductTagAsync(tag); + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.ProductTags.Deleted")); + + return RedirectToAction("ProductTags"); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_CREATE_EDIT_DELETE)] + public virtual async Task ProductTagsDelete(ICollection selectedIds) + { + if (selectedIds == null || !selectedIds.Any()) + return NoContent(); + + var tags = await _productTagService.GetProductTagsByIdsAsync(selectedIds.ToArray()); + await _productTagService.DeleteProductTagsAsync(tags); + + return Json(new { Result = true }); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_VIEW)] + public virtual async Task EditProductTag(int id) + { + //try to get a product tag with the specified id + var productTag = await _productTagService.GetProductTagByIdAsync(id); + if (productTag == null) + return RedirectToAction("List"); + + //prepare tag model + var model = await _productModelFactory.PrepareProductTagModelAsync(null, productTag); + + return View(model); + } + + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_CREATE_EDIT_DELETE)] + public virtual async Task EditProductTag(ProductTagModel model, bool continueEditing) + { + //try to get a product tag with the specified id + var productTag = await _productTagService.GetProductTagByIdAsync(model.Id); + if (productTag == null) + return RedirectToAction("List"); + + if (ModelState.IsValid) + { + productTag.Name = model.Name; + await _productTagService.UpdateProductTagAsync(productTag); + + //locales + await UpdateLocalesAsync(productTag, model); + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.ProductTags.Updated")); + + return continueEditing ? RedirectToAction("EditProductTag", new { id = productTag.Id }) : RedirectToAction("ProductTags"); + } + + //prepare model + model = await _productModelFactory.PrepareProductTagModelAsync(model, productTag, true); + + //if we got this far, something failed, redisplay form + return View(model); + } + + #endregion + + #region Purchased with order + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task PurchasedWithOrders(ProductOrderSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareProductOrderListModelAsync(searchModel, product); + + return Json(model); + } + + #endregion + + #region Export / Import + + [HttpPost, ActionName("DownloadCatalogPDF")] + [FormValueRequired("download-catalog-pdf")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)] + public virtual async Task DownloadCatalogAsPdf(ProductSearchModel model) + { + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + model.SearchVendorId = currentVendor.Id; + } + + var categoryIds = new List { model.SearchCategoryId }; + //include subcategories + if (model.SearchIncludeSubCategories && model.SearchCategoryId > 0) + categoryIds.AddRange(await _categoryService.GetChildCategoryIdsAsync(parentCategoryId: model.SearchCategoryId, showHidden: true)); + + //0 - all (according to "ShowHidden" parameter) + //1 - published only + //2 - unpublished only + bool? overridePublished = null; + if (model.SearchPublishedId == 1) + overridePublished = true; + else if (model.SearchPublishedId == 2) + overridePublished = false; + + var products = await _productService.SearchProductsAsync(0, + categoryIds: categoryIds, + manufacturerIds: new List { model.SearchManufacturerId }, + storeId: model.SearchStoreId, + vendorId: model.SearchVendorId, + warehouseId: model.SearchWarehouseId, + productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, + keywords: model.SearchProductName, + showHidden: true, + overridePublished: overridePublished); + + try + { + byte[] bytes; + await using (var stream = new MemoryStream()) + { + await _pdfService.PrintProductsToPdfAsync(stream, products); + bytes = stream.ToArray(); + } + + return File(bytes, MimeTypes.ApplicationPdf, "pdfcatalog.pdf"); + } + catch (Exception exc) + { + await _notificationService.ErrorNotificationAsync(exc); + return RedirectToAction("List"); + } + } + + [HttpPost, ActionName("ExportToXml")] + [FormValueRequired("exportxml-all")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)] + public virtual async Task ExportXmlAll(ProductSearchModel model) + { + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + model.SearchVendorId = currentVendor.Id; + } + + var categoryIds = new List { model.SearchCategoryId }; + //include subcategories + if (model.SearchIncludeSubCategories && model.SearchCategoryId > 0) + categoryIds.AddRange(await _categoryService.GetChildCategoryIdsAsync(parentCategoryId: model.SearchCategoryId, showHidden: true)); + + //0 - all (according to "ShowHidden" parameter) + //1 - published only + //2 - unpublished only + bool? overridePublished = null; + if (model.SearchPublishedId == 1) + overridePublished = true; + else if (model.SearchPublishedId == 2) + overridePublished = false; + + var products = await _productService.SearchProductsAsync(0, + categoryIds: categoryIds, + manufacturerIds: new List { model.SearchManufacturerId }, + storeId: model.SearchStoreId, + vendorId: model.SearchVendorId, + warehouseId: model.SearchWarehouseId, + productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, + keywords: model.SearchProductName, + showHidden: true, + overridePublished: overridePublished); + + try + { + var xml = await _exportManager.ExportProductsToXmlAsync(products); + + return File(Encoding.UTF8.GetBytes(xml), MimeTypes.ApplicationXml, "products.xml"); + } + catch (Exception exc) + { + await _notificationService.ErrorNotificationAsync(exc); + return RedirectToAction("List"); + } + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)] + public virtual async Task ExportXmlSelected(string selectedIds) + { + var products = new List(); + if (selectedIds != null) + { + var ids = selectedIds + .Split(_separator, StringSplitOptions.RemoveEmptyEntries) + .Select(x => Convert.ToInt32(x)) + .ToArray(); + products.AddRange(await _productService.GetProductsByIdsAsync(ids)); + } + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + products = products.Where(p => p.VendorId == currentVendor.Id).ToList(); + } + + try + { + var xml = await _exportManager.ExportProductsToXmlAsync(products); + return File(Encoding.UTF8.GetBytes(xml), MimeTypes.ApplicationXml, "products.xml"); + } + catch (Exception exc) + { + await _notificationService.ErrorNotificationAsync(exc); + return RedirectToAction("List"); + } + } + + [HttpPost, ActionName("ExportToExcel")] + [FormValueRequired("exportexcel-all")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)] + public virtual async Task ExportExcelAll(ProductSearchModel model) + { + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + model.SearchVendorId = currentVendor.Id; + } + + var categoryIds = new List { model.SearchCategoryId }; + //include subcategories + if (model.SearchIncludeSubCategories && model.SearchCategoryId > 0) + categoryIds.AddRange(await _categoryService.GetChildCategoryIdsAsync(parentCategoryId: model.SearchCategoryId, showHidden: true)); + + //0 - all (according to "ShowHidden" parameter) + //1 - published only + //2 - unpublished only + bool? overridePublished = null; + if (model.SearchPublishedId == 1) + overridePublished = true; + else if (model.SearchPublishedId == 2) + overridePublished = false; + + var products = await _productService.SearchProductsAsync(0, + categoryIds: categoryIds, + manufacturerIds: new List { model.SearchManufacturerId }, + storeId: model.SearchStoreId, + vendorId: model.SearchVendorId, + warehouseId: model.SearchWarehouseId, + productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, + keywords: model.SearchProductName, + showHidden: true, + overridePublished: overridePublished); + + try + { + var bytes = await _exportManager.ExportProductsToXlsxAsync(products); + + return File(bytes, MimeTypes.TextXlsx, "products.xlsx"); + } + catch (Exception exc) + { + await _notificationService.ErrorNotificationAsync(exc); + + return RedirectToAction("List"); + } + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)] + public virtual async Task ExportExcelSelected(string selectedIds) + { + var products = new List(); + if (selectedIds != null) + { + var ids = selectedIds + .Split(_separator, StringSplitOptions.RemoveEmptyEntries) + .Select(x => Convert.ToInt32(x)) + .ToArray(); + products.AddRange(await _productService.GetProductsByIdsAsync(ids)); + } + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null) + { + products = products.Where(p => p.VendorId == currentVendor.Id).ToList(); + } + + try + { + var bytes = await _exportManager.ExportProductsToXlsxAsync(products); + + return File(bytes, MimeTypes.TextXlsx, "products.xlsx"); + } + catch (Exception exc) + { + await _notificationService.ErrorNotificationAsync(exc); + return RedirectToAction("List"); + } + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)] + public virtual async Task ImportExcel(IFormFile importexcelfile) + { + if (await _workContext.GetCurrentVendorAsync() != null && !_vendorSettings.AllowVendorsToImportProducts) + //a vendor can not import products + return AccessDeniedView(); + + try + { + if (importexcelfile != null && importexcelfile.Length > 0) + { + await _importManager.ImportProductsFromXlsxAsync(importexcelfile.OpenReadStream()); + } + else + { + _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Common.UploadFile")); + + return RedirectToAction("List"); + } + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Imported")); + + return RedirectToAction("List"); + } + catch (Exception exc) + { + await _notificationService.ErrorNotificationAsync(exc); + + return RedirectToAction("List"); + } + } + + #endregion + + #region Tier prices + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task TierPriceList(TierPriceSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareTierPriceListModelAsync(searchModel, product); + + return Json(model); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task TierPriceCreatePopup(int productId) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productId) + ?? throw new ArgumentException("No product found with the specified id"); + + //prepare model + var model = await _productModelFactory.PrepareTierPriceModelAsync(new TierPriceModel(), product, null); + + return View(model); + } + + [HttpPost] + [FormValueRequired("save")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task TierPriceCreatePopup(TierPriceModel model) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(model.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + if (ModelState.IsValid) + { + //fill entity from model + var tierPrice = model.ToEntity(); + tierPrice.ProductId = product.Id; + tierPrice.CustomerRoleId = model.CustomerRoleId > 0 ? model.CustomerRoleId : (int?)null; + + await _productService.InsertTierPriceAsync(tierPrice); + + ViewBag.RefreshPage = true; + + return View(model); + } + + //prepare model + model = await _productModelFactory.PrepareTierPriceModelAsync(model, product, null, true); + + //if we got this far, something failed, redisplay form + return View(model); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task TierPriceEditPopup(int id) + { + //try to get a tier price with the specified id + var tierPrice = await _productService.GetTierPriceByIdAsync(id); + if (tierPrice == null) + return RedirectToAction("List", "Product"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(tierPrice.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + //prepare model + var model = await _productModelFactory.PrepareTierPriceModelAsync(null, product, tierPrice); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task TierPriceEditPopup(TierPriceModel model) + { + //try to get a tier price with the specified id + var tierPrice = await _productService.GetTierPriceByIdAsync(model.Id); + if (tierPrice == null) + return RedirectToAction("List", "Product"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(tierPrice.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + if (ModelState.IsValid) + { + //fill entity from model + tierPrice = model.ToEntity(tierPrice); + tierPrice.CustomerRoleId = model.CustomerRoleId > 0 ? model.CustomerRoleId : (int?)null; + await _productService.UpdateTierPriceAsync(tierPrice); + + ViewBag.RefreshPage = true; + + return View(model); + } + + //prepare model + model = await _productModelFactory.PrepareTierPriceModelAsync(model, product, tierPrice, true); + + //if we got this far, something failed, redisplay form + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task TierPriceDelete(int id) + { + //try to get a tier price with the specified id + var tierPrice = await _productService.GetTierPriceByIdAsync(id) + ?? throw new ArgumentException("No tier price found with the specified id"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(tierPrice.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + await _productService.DeleteTierPriceAsync(tierPrice); + + return new NullJsonResult(); + } + + #endregion + + #region Product attributes + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeMappingList(ProductAttributeMappingSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeMappingListModelAsync(searchModel, product); + + return Json(model); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeMappingCreate(int productId) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + { + _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("This is not your product")); + return RedirectToAction("List"); + } + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeMappingModelAsync(new ProductAttributeMappingModel(), product, null); + + return View(model); + } + + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeMappingCreate(ProductAttributeMappingModel model, bool continueEditing) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(model.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + { + _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("This is not your product")); + return RedirectToAction("List"); + } + + //ensure this attribute is not mapped yet + if ((await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id)) + .Any(x => x.ProductAttributeId == model.ProductAttributeId)) + { + //redisplay form + _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.AlreadyExists")); + + model = await _productModelFactory.PrepareProductAttributeMappingModelAsync(model, product, null, true); + + return View(model); + } + + //insert mapping + var productAttributeMapping = model.ToEntity(); + + await _productAttributeService.InsertProductAttributeMappingAsync(productAttributeMapping); + await UpdateLocalesAsync(productAttributeMapping, model); + + //predefined values + var predefinedValues = await _productAttributeService.GetPredefinedProductAttributeValuesAsync(model.ProductAttributeId); + foreach (var predefinedValue in predefinedValues) + { + var pav = new ProductAttributeValue + { + ProductAttributeMappingId = productAttributeMapping.Id, + AttributeValueType = AttributeValueType.Simple, + Name = predefinedValue.Name, + PriceAdjustment = predefinedValue.PriceAdjustment, + PriceAdjustmentUsePercentage = predefinedValue.PriceAdjustmentUsePercentage, + WeightAdjustment = predefinedValue.WeightAdjustment, + Cost = predefinedValue.Cost, + IsPreSelected = predefinedValue.IsPreSelected, + DisplayOrder = predefinedValue.DisplayOrder + }; + await _productAttributeService.InsertProductAttributeValueAsync(pav); + + //locales + var languages = await _languageService.GetAllLanguagesAsync(true); + + //localization + foreach (var lang in languages) + { + var name = await _localizationService.GetLocalizedAsync(predefinedValue, x => x.Name, lang.Id, false, false); + if (!string.IsNullOrEmpty(name)) + await _localizedEntityService.SaveLocalizedValueAsync(pav, x => x.Name, name, lang.Id); + } + } + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Added")); + + if (!continueEditing) + { + //select an appropriate card + SaveSelectedCardName("product-product-attributes"); + return RedirectToAction("Edit", new { id = product.Id }); + } + + return RedirectToAction("ProductAttributeMappingEdit", new { id = productAttributeMapping.Id }); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeMappingEdit(int id) + { + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(id) + ?? throw new ArgumentException("No product attribute mapping found with the specified id"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + { + _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("This is not your product")); + return RedirectToAction("List"); + } + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeMappingModelAsync(null, product, productAttributeMapping); + + return View(model); + } + + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeMappingEdit(ProductAttributeMappingModel model, bool continueEditing, IFormCollection form) + { + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(model.Id) + ?? throw new ArgumentException("No product attribute mapping found with the specified id"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + { + _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("This is not your product")); + return RedirectToAction("List"); + } + + //ensure this attribute is not mapped yet + if ((await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id)) + .Any(x => x.ProductAttributeId == model.ProductAttributeId && x.Id != productAttributeMapping.Id)) + { + //redisplay form + _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.AlreadyExists")); + + model = await _productModelFactory.PrepareProductAttributeMappingModelAsync(model, product, productAttributeMapping, true); + + return View(model); + } + + //fill entity from model + productAttributeMapping = model.ToEntity(productAttributeMapping); + await _productAttributeService.UpdateProductAttributeMappingAsync(productAttributeMapping); + + await UpdateLocalesAsync(productAttributeMapping, model); + + await SaveConditionAttributesAsync(productAttributeMapping, model.ConditionModel, form); + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Updated")); + + if (!continueEditing) + { + //select an appropriate card + SaveSelectedCardName("product-product-attributes"); + return RedirectToAction("Edit", new { id = product.Id }); + } + + return RedirectToAction("ProductAttributeMappingEdit", new { id = productAttributeMapping.Id }); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeMappingDelete(int id) + { + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(id) + ?? throw new ArgumentException("No product attribute mapping found with the specified id"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //check if existed combinations contains the specified attribute + var existedCombinations = await _productAttributeService.GetAllProductAttributeCombinationsAsync(product.Id); + if (existedCombinations?.Any() == true) + { + foreach (var combination in existedCombinations) + { + var mappings = await _productAttributeParser + .ParseProductAttributeMappingsAsync(combination.AttributesXml); + + if (mappings?.Any(m => m.Id == productAttributeMapping.Id) == true) + { + _notificationService.ErrorNotification( + string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.AlreadyExistsInCombination"), + await _productAttributeFormatter.FormatAttributesAsync(product, combination.AttributesXml, await _workContext.GetCurrentCustomerAsync(), await _storeContext.GetCurrentStoreAsync(), ", "))); + + return RedirectToAction("ProductAttributeMappingEdit", new { id = productAttributeMapping.Id }); + } + } + } + + await _productAttributeService.DeleteProductAttributeMappingAsync(productAttributeMapping); + + _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Deleted")); + + //select an appropriate card + SaveSelectedCardName("product-product-attributes"); + return RedirectToAction("Edit", new { id = productAttributeMapping.ProductId }); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeValueList(ProductAttributeValueSearchModel searchModel) + { + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(searchModel.ProductAttributeMappingId) + ?? throw new ArgumentException("No product attribute mapping found with the specified id"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeValueListModelAsync(searchModel, productAttributeMapping); + + return Json(model); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeValueCreatePopup(int productAttributeMappingId) + { + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(productAttributeMappingId) + ?? throw new ArgumentException("No product attribute mapping found with the specified id"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeValueModelAsync(new ProductAttributeValueModel(), productAttributeMapping, null); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeValueCreatePopup(ProductAttributeValueModel model) + { + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(model.ProductAttributeMappingId); + if (productAttributeMapping == null) + return RedirectToAction("List", "Product"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + if (productAttributeMapping.AttributeControlType == AttributeControlType.ColorSquares) + { + //ensure valid color is chosen/entered + if (string.IsNullOrEmpty(model.ColorSquaresRgb)) + ModelState.AddModelError(string.Empty, "Color is required"); + try + { + //ensure color is valid (can be instantiated) + System.Drawing.ColorTranslator.FromHtml(model.ColorSquaresRgb); + } + catch (Exception exc) + { + ModelState.AddModelError(string.Empty, exc.Message); + } + } + + //ensure a picture is uploaded + if (productAttributeMapping.AttributeControlType == AttributeControlType.ImageSquares && model.ImageSquaresPictureId == 0) + { + ModelState.AddModelError(string.Empty, "Image is required"); + } + + if (ModelState.IsValid) + { + //fill entity from model + var pav = model.ToEntity(); + + pav.Quantity = model.CustomerEntersQty ? 1 : model.Quantity; + + await _productAttributeService.InsertProductAttributeValueAsync(pav); + await UpdateLocalesAsync(pav, model); + await SaveAttributeValuePicturesAsync(product, pav, model); + + ViewBag.RefreshPage = true; + + return View(model); + } + + //prepare model + model = await _productModelFactory.PrepareProductAttributeValueModelAsync(model, productAttributeMapping, null, true); + + //if we got this far, something failed, redisplay form + return View(model); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeValueEditPopup(int id) + { + //try to get a product attribute value with the specified id + var productAttributeValue = await _productAttributeService.GetProductAttributeValueByIdAsync(id); + if (productAttributeValue == null) + return RedirectToAction("List", "Product"); + + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(productAttributeValue.ProductAttributeMappingId); + if (productAttributeMapping == null) + return RedirectToAction("List", "Product"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeValueModelAsync(null, productAttributeMapping, productAttributeValue); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeValueEditPopup(ProductAttributeValueModel model) + { + //try to get a product attribute value with the specified id + var productAttributeValue = await _productAttributeService.GetProductAttributeValueByIdAsync(model.Id); + if (productAttributeValue == null) + return RedirectToAction("List", "Product"); + + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(productAttributeValue.ProductAttributeMappingId); + if (productAttributeMapping == null) + return RedirectToAction("List", "Product"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + if (productAttributeMapping.AttributeControlType == AttributeControlType.ColorSquares) + { + //ensure valid color is chosen/entered + if (string.IsNullOrEmpty(model.ColorSquaresRgb)) + ModelState.AddModelError(string.Empty, "Color is required"); + try + { + //ensure color is valid (can be instantiated) + System.Drawing.ColorTranslator.FromHtml(model.ColorSquaresRgb); + } + catch (Exception exc) + { + ModelState.AddModelError(string.Empty, exc.Message); + } + } + + //ensure a picture is uploaded + if (productAttributeMapping.AttributeControlType == AttributeControlType.ImageSquares && model.ImageSquaresPictureId == 0) + { + ModelState.AddModelError(string.Empty, "Image is required"); + } + + if (ModelState.IsValid) + { + //fill entity from model + productAttributeValue = model.ToEntity(productAttributeValue); + productAttributeValue.Quantity = model.CustomerEntersQty ? 1 : model.Quantity; + await _productAttributeService.UpdateProductAttributeValueAsync(productAttributeValue); + + await UpdateLocalesAsync(productAttributeValue, model); + await SaveAttributeValuePicturesAsync(product, productAttributeValue, model); + + ViewBag.RefreshPage = true; + + return View(model); + } + + //prepare model + model = await _productModelFactory.PrepareProductAttributeValueModelAsync(model, productAttributeMapping, productAttributeValue, true); + + //if we got this far, something failed, redisplay form + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeValueDelete(int id) + { + //try to get a product attribute value with the specified id + var productAttributeValue = await _productAttributeService.GetProductAttributeValueByIdAsync(id) + ?? throw new ArgumentException("No product attribute value found with the specified id"); + + //try to get a product attribute mapping with the specified id + var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(productAttributeValue.ProductAttributeMappingId) + ?? throw new ArgumentException("No product attribute mapping found with the specified id"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //check if existed combinations contains the specified attribute value + var existedCombinations = await _productAttributeService.GetAllProductAttributeCombinationsAsync(product.Id); + if (existedCombinations?.Any() == true) + { + foreach (var combination in existedCombinations) + { + var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(combination.AttributesXml); + + if (attributeValues.Where(attribute => attribute.Id == id).Any()) + { + return Conflict(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.AlreadyExistsInCombination"), + await _productAttributeFormatter.FormatAttributesAsync(product, combination.AttributesXml, await _workContext.GetCurrentCustomerAsync(), await _storeContext.GetCurrentStoreAsync(), ", "))); + } + } + } + + await _productAttributeService.DeleteProductAttributeValueAsync(productAttributeValue); + + return new NullJsonResult(); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task AssociateProductToAttributeValuePopup() + { + //prepare model + var model = await _productModelFactory.PrepareAssociateProductToAttributeValueSearchModelAsync(new AssociateProductToAttributeValueSearchModel()); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task AssociateProductToAttributeValuePopupList(AssociateProductToAttributeValueSearchModel searchModel) + { + //prepare model + var model = await _productModelFactory.PrepareAssociateProductToAttributeValueListModelAsync(searchModel); + + return Json(model); + } + + [HttpPost] + [FormValueRequired("save")] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task AssociateProductToAttributeValuePopup([Bind(Prefix = nameof(AssociateProductToAttributeValueModel))] AssociateProductToAttributeValueModel model) + { + //try to get a product with the specified id + var associatedProduct = await _productService.GetProductByIdAsync(model.AssociatedToProductId); + if (associatedProduct == null) + return Content("Cannot load a product"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && associatedProduct.VendorId != currentVendor.Id) + return Content("This is not your product"); + + ViewBag.RefreshPage = true; + ViewBag.productId = associatedProduct.Id; + ViewBag.productName = associatedProduct.Name; + + return View(new AssociateProductToAttributeValueSearchModel()); + } + + //action displaying notification (warning) to a store owner when associating some product + public virtual async Task AssociatedProductGetWarnings(int productId) + { + var associatedProduct = await _productService.GetProductByIdAsync(productId); + if (associatedProduct == null) + return Json(new { Result = string.Empty }); + + //attributes + if (await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(associatedProduct.Id) is IList mapping && mapping.Any()) + { + if (mapping.Any(attribute => attribute.IsRequired)) + return Json(new { Result = await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.Fields.AssociatedProduct.HasRequiredAttributes") }); + + return Json(new { Result = await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.Fields.AssociatedProduct.HasAttributes") }); + } + + //gift card + if (associatedProduct.IsGiftCard) + { + return Json(new { Result = await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.Fields.AssociatedProduct.GiftCard") }); + } + + //downloadable product + if (associatedProduct.IsDownload) + { + return Json(new { Result = await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.Fields.AssociatedProduct.Downloadable") }); + } + + return Json(new { Result = string.Empty }); + } + + #endregion + + #region Product attribute combinations + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeCombinationList(ProductAttributeCombinationSearchModel searchModel) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeCombinationListModelAsync(searchModel, product); + + return Json(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeCombinationDelete(int id) + { + //try to get a combination with the specified id + var combination = await _productAttributeService.GetProductAttributeCombinationByIdAsync(id) + ?? throw new ArgumentException("No product attribute combination found with the specified id"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(combination.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + await _productAttributeService.DeleteProductAttributeCombinationAsync(combination); + + return new NullJsonResult(); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeCombinationCreatePopup(int productId) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productId); + if (product == null) + return RedirectToAction("List", "Product"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(new ProductAttributeCombinationModel(), product, null); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + public virtual async Task ProductAttributeCombinationCreatePopup(int productId, ProductAttributeCombinationModel model, IFormCollection form) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productId); + if (product == null) + return RedirectToAction("List", "Product"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + //attributes + var warnings = new List(); + var attributesXml = await GetAttributesXmlForProductAttributeCombinationAsync(form, warnings, product.Id); + + //check whether the attribute value is specified + if (string.IsNullOrEmpty(attributesXml)) + warnings.Add(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.Alert.FailedValue")); + + warnings.AddRange(await _shoppingCartService.GetShoppingCartItemAttributeWarningsAsync(await _workContext.GetCurrentCustomerAsync(), + ShoppingCartType.ShoppingCart, product, 1, attributesXml, true)); + + //check whether the same attribute combination already exists + var existingCombination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml); + if (existingCombination != null) + warnings.Add(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.AlreadyExists")); + + if (!warnings.Any()) + { + //save combination + var combination = model.ToEntity(); + + //fill attributes + combination.AttributesXml = attributesXml; + + await _productAttributeService.InsertProductAttributeCombinationAsync(combination); + + await SaveAttributeCombinationPicturesAsync(product, combination, model); + + //quantity change history + await _productService.AddStockQuantityHistoryEntryAsync(product, combination.StockQuantity, combination.StockQuantity, + message: await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Combination.Edit"), combinationId: combination.Id); + + ViewBag.RefreshPage = true; + + return View(model); + } + + //prepare model + model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(model, product, null, true); + model.Warnings = warnings; + + //if we got this far, something failed, redisplay form + return View(model); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeCombinationGeneratePopup(int productId) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productId); + if (product == null) + return RedirectToAction("List", "Product"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(new ProductAttributeCombinationModel(), product, null); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeCombinationGeneratePopup(IFormCollection form, ProductAttributeCombinationModel model) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(model.ProductId); + if (product == null) + return RedirectToAction("List", "Product"); + + var allowedAttributeIds = form.Keys.Where(key => key.Contains("attribute_value_")) + .Select(key => int.TryParse(form[key], out var id) ? id : 0).Where(id => id > 0).ToList(); + + var requiredAttributeNames = await (await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id)) + .Where(pam => pam.IsRequired) + .Where(pam => !pam.IsNonCombinable()) + .WhereAwait(async pam => !(await _productAttributeService.GetProductAttributeValuesAsync(pam.Id)).Any(v => allowedAttributeIds.Any(id => id == v.Id))) + .SelectAwait(async pam => (await _productAttributeService.GetProductAttributeByIdAsync(pam.ProductAttributeId)).Name).ToListAsync(); + + if (requiredAttributeNames.Any()) + { + model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(model, product, null, true); + var pavModels = model.ProductAttributes.SelectMany(pa => pa.Values) + .Where(v => allowedAttributeIds.Any(id => id == v.Id)) + .ToList(); + foreach (var pavModel in pavModels) + { + pavModel.Checked = "checked"; + } + + model.Warnings.Add(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.SelectRequiredAttributes"), string.Join(", ", requiredAttributeNames))); + + return View(model); + } + + await GenerateAttributeCombinationsAsync(product, allowedAttributeIds); + + ViewBag.RefreshPage = true; + + return View(new ProductAttributeCombinationModel()); + } + + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeCombinationEditPopup(int id) + { + //try to get a combination with the specified id + var combination = await _productAttributeService.GetProductAttributeCombinationByIdAsync(id); + if (combination == null) + return RedirectToAction("List", "Product"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(combination.ProductId); + if (product == null) + return RedirectToAction("List", "Product"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + //prepare model + var model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(null, product, combination); + + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductAttributeCombinationEditPopup(ProductAttributeCombinationModel model, IFormCollection form) + { + //try to get a combination with the specified id + var combination = await _productAttributeService.GetProductAttributeCombinationByIdAsync(model.Id); + if (combination == null) + return RedirectToAction("List", "Product"); + + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(combination.ProductId); + if (product == null) + return RedirectToAction("List", "Product"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return RedirectToAction("List", "Product"); + + //attributes + var warnings = new List(); + var attributesXml = await GetAttributesXmlForProductAttributeCombinationAsync(form, warnings, product.Id); + + //check whether the attribute value is specified + if (string.IsNullOrEmpty(attributesXml)) + warnings.Add(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.Alert.FailedValue")); + + warnings.AddRange(await _shoppingCartService.GetShoppingCartItemAttributeWarningsAsync(await _workContext.GetCurrentCustomerAsync(), + ShoppingCartType.ShoppingCart, product, 1, attributesXml, true)); + + //check whether the same attribute combination already exists + var existingCombination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml); + if (existingCombination != null && existingCombination.Id != model.Id && existingCombination.AttributesXml.Equals(attributesXml)) + warnings.Add(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.AlreadyExists")); + + if (!warnings.Any() && ModelState.IsValid) + { + var previousStockQuantity = combination.StockQuantity; + + //save combination + //fill entity from model + combination = model.ToEntity(combination); + combination.AttributesXml = attributesXml; + + await _productAttributeService.UpdateProductAttributeCombinationAsync(combination); + + await SaveAttributeCombinationPicturesAsync(product, combination, model); + + //quantity change history + await _productService.AddStockQuantityHistoryEntryAsync(product, combination.StockQuantity - previousStockQuantity, combination.StockQuantity, + message: await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Combination.Edit"), combinationId: combination.Id); + + ViewBag.RefreshPage = true; + + return View(model); + } + + //prepare model + model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(model, product, combination, true); + model.Warnings = warnings; + + //if we got this far, something failed, redisplay form + return View(model); + } + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task GenerateAllAttributeCombinations(int productId) + { + //try to get a product with the specified id + var product = await _productService.GetProductByIdAsync(productId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + await GenerateAttributeCombinationsAsync(product); + + return Json(new { Success = true }); + } + + #endregion + + #region Product editor settings + + [HttpPost] + [CheckPermission(StandardPermission.Configuration.MANAGE_SETTINGS)] + public virtual async Task SaveProductEditorSettings(ProductModel model, string returnUrl = "") + { + //vendors cannot manage these settings + if (await _workContext.GetCurrentVendorAsync() != null) + return RedirectToAction("List"); + + var productEditorSettings = await _settingService.LoadSettingAsync(); + productEditorSettings = model.ProductEditorSettingsModel.ToSettings(productEditorSettings); + await _settingService.SaveSettingAsync(productEditorSettings); + + //product list + if (string.IsNullOrEmpty(returnUrl)) + return RedirectToAction("List"); + + //prevent open redirection attack + if (!Url.IsLocalUrl(returnUrl)) + return RedirectToAction("List"); + + return Redirect(returnUrl); + } + + #endregion + + #region Stock quantity history + + [HttpPost] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task StockQuantityHistory(StockQuantityHistorySearchModel searchModel) + { + var product = await _productService.GetProductByIdAsync(searchModel.ProductId) + ?? throw new ArgumentException("No product found with the specified id"); + + //a vendor should have access only to his products + var currentVendor = await _workContext.GetCurrentVendorAsync(); + if (currentVendor != null && product.VendorId != currentVendor.Id) + return Content("This is not your product"); + + //prepare model + var model = await _productModelFactory.PrepareStockQuantityHistoryListModelAsync(searchModel, product); + + return Json(model); + } + + #endregion + + #endregion + + #region Nested class + + protected class BulkEditData + { + protected bool _updated; + protected bool _created; + protected int _defaultTaxCategoryId; + protected int _vendorId; + + public BulkEditData(int defaultTaxCategoryId, int vendorId) + { + _defaultTaxCategoryId = defaultTaxCategoryId; + _vendorId = vendorId; + } + + public bool IsSelected { get; set; } + public string Name { get; set; } + public string Sku { get; set; } + public decimal Price { get; set; } + public decimal OldPrice { get; set; } + public int Quantity { get; set; } + public bool IsPublished { get; set; } + public Product Product { get; set; } + + public bool NeedToUpdate(bool selected) + { + if (selected && !IsSelected) + return false; + + if (Product == null) + return false; + + if (_updated) + return true; + + return isStringValueChanged(Product.Name, Name) || + isStringValueChanged(Product.Sku, Sku) || + !Product.Price.Equals(Price) || + !Product.OldPrice.Equals(OldPrice) || + !Product.StockQuantity.Equals(Quantity) || + !Product.Published.Equals(IsPublished); + + bool isStringValueChanged(string oldValue, string newValue) + { + if (string.IsNullOrEmpty(oldValue)) + return !string.IsNullOrEmpty(newValue); + + return !oldValue.Equals(newValue); + } + } + + public bool NeedToCreate(bool selected) + { + if (selected && !IsSelected) + return false; + + if (Product != null) + return false; + + return true; + } + + public Product UpdateProduct(bool selected) + { + if (!NeedToUpdate(selected) || _updated) + return Product; + + Product.Name = Name; + Product.Sku = Sku; + Product.Price = Price; + Product.OldPrice = OldPrice; + Product.StockQuantity = Quantity; + Product.Published = IsPublished; + + _updated = true; + + return Product; + } + + public Product CreateProduct(bool selected) + { + if (!NeedToCreate(selected) || _created) + return Product; + + Product = new Product + { + Name = Name, + Sku = Sku, + Price = Price, + OldPrice = OldPrice, + StockQuantity = Quantity, + Published = IsPublished, + //set default values for the new model + MaximumCustomerEnteredPrice = 1000, + MaxNumberOfDownloads = 10, + RecurringCycleLength = 100, + RecurringTotalCycles = 10, + RentalPriceLength = 1, + NotifyAdminForQuantityBelow = 1, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + TaxCategoryId = _defaultTaxCategoryId, + UnlimitedDownloads = true, + IsShipEnabled = true, + AllowCustomerReviews = true, + VisibleIndividually = true, + ProductType = ProductType.SimpleProduct, + VendorId = _vendorId + }; + + _created = true; + + return Product; + } + } + + #endregion +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerController.cs new file mode 100644 index 0000000..4b15dc7 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + +namespace DevExtreme.NETCore.Demos.Controllers +{ + public class FileManagerController : Controller + { + public IActionResult BindingToFileSystem() + { + return View(); + } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs new file mode 100644 index 0000000..ab40297 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs @@ -0,0 +1,44 @@ +using DevExtreme.AspNet.Mvc.FileManagement; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; + +namespace DevExtreme.NETCore.Demos.Controllers +{ + public class FileManagerScriptsApiController : Controller + { + IWebHostEnvironment _webHostEnvironment; + + public FileManagerScriptsApiController(IWebHostEnvironment webHostEnvironment) + { + _webHostEnvironment = webHostEnvironment; + } + + + [HttpGet] + [HttpPost] + [Route("api/file-manager-file-system", Name = "FileManagementFileSystemApi")] + public object FileSystem(FileSystemCommand command, string arguments, int orderId) + { + var valami = new List(); + + var config = new FileSystemConfiguration + { + Request = Request, + FileSystemProvider = new PhysicalFileSystemProvider(_webHostEnvironment.ContentRootPath + $"/wwwroot/uploads/orders/{orderId}"), + //uncomment the code below to enable file/folder management + //AllowCopy = true, + //AllowCreate = true, + //AllowMove = true, + //AllowDelete = true, + //AllowRename = true, + //AllowUpload = true, + //AllowDownload = true, + AllowedFileExtensions = new[] { ".pdf", ".jpg", ".jpeg" } + }; + var processor = new FileSystemCommandProcessor(config); + var result = processor.Execute(command, arguments); + return result.GetClientCommandResult(); + } + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs index f135f18..971499d 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs @@ -42,6 +42,19 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers var testGridModel2 = new TestGridModel(); testGridModel2.GridName = "Orders"; testGridModel2.ViewComponentName = "ShippingDocumentGridComponent"; + testGridModel2.ChildGrids = new List(); + var childGrid1 = new TestGridModel + { + GridName = "TestGrid", + ViewComponentName = "TestGridComponent", + ParentGridId = testGridModel2.Id, + ChildGrids = new List() + }; + testGridModel2.Configuration = new GridConfiguration(); + testGridModel2.Configuration.ShowChildGridsAsTabs = true; + testGridModel2.ChildGrids.Add(childGrid1); + + testPageModel.Grids.Add(testGridModel2); var testGridModel = new TestGridModel(); @@ -58,6 +71,22 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/Test.cshtml", testPageModel); } + [HttpPost] + public async Task LoadChildGrid([FromBody] LoadChildGridRequest request) + { + // request.contextId is the actual row ID (data.Id from DevExtreme) + // request.childModel is the full TestGridModel object + + // Add the context ID to the model's DataContext + if (request.ChildModel.DataContext == null) + request.ChildModel.DataContext = new Dictionary(); + + request.ChildModel.DataContext["contextId"] = request.ContextId; + + // Invoke the view component with the full model + return ViewComponent(request.ChildModel.ViewComponentLocation, new { model = request.ChildModel }); + } + [HttpGet] public async Task GetShippings() { @@ -135,6 +164,17 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return Json(model); } + + + + // Request model for deserialization + public class LoadChildGridRequest + { + public int ContextId { get; set; } // The actual row ID from data.Id + public TestGridModel ChildModel { get; set; } // The full child grid model + } + + [HttpPost] [RequestSizeLimit(10485760)] // 10MB [RequestFormLimits(MultipartBodyLengthLimit = 10485760)] @@ -254,7 +294,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers Console.WriteLine(extractedPartnerData.TaxId); } - if (extractedPartnerData.Country != null) { + if (extractedPartnerData.Country != null) + { Console.WriteLine(extractedPartnerData.Country); } diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/Catalog/ProductListModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/Catalog/ProductListModel.cs new file mode 100644 index 0000000..dbc0216 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/Catalog/ProductListModel.cs @@ -0,0 +1,10 @@ +using Nop.Web.Framework.Models; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.Catalog; + +/// +/// Represents a product list model +/// +public partial record ProductListModel : BasePagedListModel +{ +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestGridModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestGridModel.cs index 29253f2..b34232a 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestGridModel.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestGridModel.cs @@ -1,18 +1,185 @@ -//using Nop.Web.Framework.Models; -//using Nop.Web.Framework.Mvc.ModelBinding; -//using System.ComponentModel.DataAnnotations; - -namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models { + + + /// + /// Represents a grid with potential child grids + /// public class TestGridModel { - public Guid Id = Guid.NewGuid(); + public TestGridModel() + { + Id = Guid.NewGuid(); + ChildGrids = new List(); + Configuration = new GridConfiguration(); + } + + // Identity + public Guid Id { get; set; } public string GridName { get; set; } - //public string GridControllerName { get; set; } - //public string GridEndpointName { get; set; } + + // View Component Information public string ViewComponentName { get; set; } public string ViewComponentLocation { get; set; } - public int? ParentRowId { get; set; } + // Hierarchy + public Guid? ParentGridId { get; set; } + public int Level { get; set; } // 0 = top level, 1 = first nested, etc. + public List ChildGrids { get; set; } + + // Grid Behavior Configuration + public GridConfiguration Configuration { get; set; } + + // Data Context (optional - for passing entity IDs or filter params) + public Dictionary DataContext { get; set; } + } + + /// + /// Configuration for grid rendering and behavior + /// + public class GridConfiguration + { + // Display Settings + public bool ShowChildGridsAsTabs { get; set; } = true; + public bool ShowChildGridsAsAccordion { get; set; } = false; + public bool ShowChildGridsInline { get; set; } = false; + + // Rendering Options + public string ChildGridContainerCssClass { get; set; } = "nested-grid-container"; + public bool LazyLoadChildren { get; set; } = false; + public bool CollapseByDefault { get; set; } = false; + + // Data Loading + public string ChildDataEndpoint { get; set; } + public bool RequiresParentRowSelection { get; set; } = false; + + // Metadata + public string Description { get; set; } + public int DisplayOrder { get; set; } + } + + /// + /// Builder class for easier model construction + /// + public class TestPageModelBuilder + { + private readonly TestPageModel _model; + private readonly Dictionary _gridLookup; + + public TestPageModelBuilder() + { + _model = new TestPageModel(); + _gridLookup = new Dictionary(); + } + + public TestPageModelBuilder AddRootGrid(TestGridModel grid) + { + grid.Level = 0; + grid.ParentGridId = null; + _model.Grids.Add(grid); + _gridLookup[grid.Id] = grid; + return this; + } + + public TestPageModelBuilder AddChildGrid(Guid parentId, TestGridModel childGrid) + { + if (_gridLookup.TryGetValue(parentId, out var parentGrid)) + { + childGrid.Level = parentGrid.Level + 1; + childGrid.ParentGridId = parentId; + parentGrid.ChildGrids.Add(childGrid); + _gridLookup[childGrid.Id] = childGrid; + } + return this; + } + + public TestPageModel Build() + { + return _model; + } + } + + /// + /// Example usage helper + /// + public static class TestPageModelExample + { + public static TestPageModel CreateSampleModel() + { + var builder = new TestPageModelBuilder(); + + // Level 0 - Root Grids + var customersGrid = new TestGridModel + { + GridName = "Customers", + ViewComponentName = "CustomerGrid", + Configuration = new GridConfiguration + { + RequiresParentRowSelection = false, + Description = "Main customer list" + } + }; + + var ordersRootGrid = new TestGridModel + { + GridName = "All Orders", + ViewComponentName = "OrderGrid" + }; + + builder.AddRootGrid(customersGrid); + builder.AddRootGrid(ordersRootGrid); + + // Level 1 - Child of Customers + var customerOrdersGrid = new TestGridModel + { + GridName = "Customer Orders", + ViewComponentName = "CustomerOrderGrid", + Configuration = new GridConfiguration + { + RequiresParentRowSelection = true, + ShowChildGridsAsTabs = true + } + }; + + var customerAddressesGrid = new TestGridModel + { + GridName = "Addresses", + ViewComponentName = "CustomerAddressGrid" + }; + + builder.AddChildGrid(customersGrid.Id, customerOrdersGrid); + builder.AddChildGrid(customersGrid.Id, customerAddressesGrid); + + // Level 2 - Child of Customer Orders + var orderItemsGrid = new TestGridModel + { + GridName = "Order Items", + ViewComponentName = "OrderItemGrid", + Configuration = new GridConfiguration + { + RequiresParentRowSelection = true + } + }; + + var orderShipmentsGrid = new TestGridModel + { + GridName = "Shipments", + ViewComponentName = "OrderShipmentGrid" + }; + + builder.AddChildGrid(customerOrdersGrid.Id, orderItemsGrid); + builder.AddChildGrid(customerOrdersGrid.Id, orderShipmentsGrid); + + // Level 3 - Child of Order Items + var itemAttributesGrid = new TestGridModel + { + GridName = "Item Attributes", + ViewComponentName = "ItemAttributeGrid" + }; + + builder.AddChildGrid(orderItemsGrid.Id, itemAttributesGrid); + + return builder.Build(); + } } } \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestPageModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestPageModel.cs index a1a343b..10671a1 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestPageModel.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestPageModel.cs @@ -6,6 +6,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models { public class TestPageModel { + public TestPageModel() + { + Grids = new List(); + } + public List Grids { get; set; } } } \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml new file mode 100644 index 0000000..b721546 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml @@ -0,0 +1,118 @@ +@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel +@using DevExtreme.AspNet.Mvc + +@{ + if (Model.DataContext["contextId"] != null) + { + var contextId = Model.DataContext["contextId"]; + +

contextId

+
+ }) + // var gridId = $"dataGrid_{Guid.NewGuid():N}"; +} + + +
+
+ + @Model.DataContext["contextId"], @Model.GridName, @Model.ViewComponentName + @*
+ @(Html.DevExtreme().FileUploader() + .ID("shippingDocumentUploader-" + contextId) + .Name("files") + .Multiple(true) + .Accept("application/pdf") + .UploadMode(FileUploadMode.UseForm) + ) + + <% if (data.PartnerId) { %> + + <% } %> + @(Html.DevExtreme().Button() + .Text("Upload Files") + .Type(ButtonType.Success) + .UseSubmitBehavior(true) + ) +
+
+
+

Selected Files

+
+
*@ +
+
+ @* @(Html.DevExtreme().FileManager() + .ID("fileManager_"+ contextId) + .FileSystemProvider(provider => provider.Remote() + .Url(Url.RouteUrl("FileManagementFileSystemApi")) + .BeforeAjaxSend(@ + function(e) { + e.ajaxSettings.data.orderId = data.Id; + } + )) + .Permissions(permissions => { + permissions.Download(true); + permissions.Upload(true); + }) + .AllowedFileExtensions(new[] { ".pdf", ".jpg", ".jpeg" }) + ) *@ +
+
+ + + \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml index 9a7d5c2..8648ceb 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml @@ -344,7 +344,7 @@ { Title = "Needs Measurement", Width = "100", - Render = new RenderCustom("renderColumnNeedsMeasurement"), + Render = new RenderCustom("renderColumnIsMeasurable"), ClassName = NopColumnClassDefaults.CenterAll }); @@ -441,7 +441,7 @@ return `${textRenderer(row.CustomerFullName)}
${data} `; } - function renderColumnNeedsMeasurement(data, type, row, meta) { + function renderColumnIsMeasurable(data, type, row, meta) { if(data === true) { return 'Yes'; } diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml index 016abc1..5f4527e 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml @@ -1,11 +1,6 @@ @model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel @using DevExtreme.AspNet.Mvc -@{ - var contextId = Model; - // var gridId = $"dataGrid_{Guid.NewGuid():N}"; -} -
@( Html.DevExtreme().DataGrid() @@ -27,23 +22,21 @@ editing.AllowDeleting(true); }) .Columns(c => { - c.Add().DataField("Id").AllowEditing(false); c.Add().DataField("Partner.Name").AllowEditing(false); c.Add() - .Caption("Items in order") - .DataType(GridColumnDataType.Number) - .CalculateCellValue("calculateItemsCount").AllowEditing(false); + .Caption("Items in order") + .DataType(GridColumnDataType.Number) + .CalculateCellValue("calculateItemsCount").AllowEditing(false); c.Add().DataField("PartnerId"); - c.Add().DataField("DocumentIdNumber"); - c.Add().DataField("IsAllMeasured"); - - c.Add() - .Caption("Completed") - .DataType(GridColumnDataType.Boolean) - .CalculateCellValue("calculateCellValue").AllowEditing(false); - }) - .Toolbar(toolbar => { + c.Add().DataField("DocumentIdNumber"); + c.Add().DataField("IsAllMeasured"); + c.Add() + .Caption("Completed") + .DataType(GridColumnDataType.Boolean) + .CalculateCellValue("calculateCellValue").AllowEditing(false); + }) + .Toolbar(toolbar => { toolbar.Items(items => { items.Add() .Name("addRowButton") @@ -60,244 +53,143 @@ ); }); }) - .MasterDetail(md => { - md.Enabled(true); - md.Template(@ -
<%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:
-
-
- -
- -
-
- -
+ }
-
- -
); - }) -) -
- + } +} - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml new file mode 100644 index 0000000..f494da9 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml @@ -0,0 +1,117 @@ +@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel +@using System.Text.Json + +
+
+

Test Child Grid View Component

+
+
+
Grid Information:
+ + + + + + + + + + + + + + + + + + + + + +
Grid ID:@Model.Id
Grid Name:@Model.GridName
View Component Name:@Model.ViewComponentName
Level:@Model.Level
Parent Grid ID:@(Model.ParentGridId?.ToString() ?? "None")
+ + @if (Model.DataContext != null && Model.DataContext.Any()) + { +
Data Context:
+ + + + + + + + + + @foreach (var kvp in Model.DataContext) + { + + + + + + } + +
KeyValueType
@kvp.Key@kvp.Value@kvp.Value?.GetType().Name
+ + @if (Model.DataContext.ContainsKey("contextId")) + { +
+ Context ID Found: @Model.DataContext["contextId"] +
+ } + else + { +
+ Warning: No contextId found in DataContext +
+ } + } + else + { +
+ No Data Context available +
+ } + + @if (Model.Configuration != null) + { +
Configuration:
+ + + + + + + + + + + + + +
Show Child Grids As Tabs:@Model.Configuration.ShowChildGridsAsTabs
Requires Parent Row Selection:@Model.Configuration.RequiresParentRowSelection
Description:@(Model.Configuration.Description ?? "N/A")
+ } + + @if (Model.ChildGrids != null && Model.ChildGrids.Any()) + { +
+ Child Grids: This grid has @Model.ChildGrids.Count child grid(s) +
    + @foreach (var child in Model.ChildGrids) + { +
  • @child.GridName (Level @child.Level)
  • + } +
+
+ } + else + { +
+ No child grids +
+ } + +
Full Model JSON:
+
@JsonSerializer.Serialize(Model, new JsonSerializerOptions { WriteIndented = true })
+
+
\ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Product/List.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Product/List.cshtml new file mode 100644 index 0000000..cfa08e8 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Product/List.cshtml @@ -0,0 +1,466 @@ +@model ProductSearchModel + +@using Nop.Core.Domain.Catalog; + +@{ + //page title + ViewBag.PageTitle = T("Admin.Catalog.Products").Text; + //active menu item (system name) + NopHtml.SetActiveMenuItemSystemName("Products"); +} + +@{ + const string hideSearchBlockAttributeName = "ProductListPage.HideSearchBlock"; + var hideSearchBlock = await genericAttributeService.GetAttributeAsync(await workContext.GetCurrentCustomerAsync(), hideSearchBlockAttributeName); +} + +@if (Model.LicenseCheckModel.BlockPages != true) +{ +
+
+

+ Fruitbank @T("Admin.Catalog.Products") +

+
+ + + @T("Admin.Common.AddNew") + + + + @T("Admin.Catalog.Products.BulkEdit") + + +
+ + + +
+ @if (!Model.IsLoggedInAsVendor || Model.AllowVendorsToImportProducts) + { + //a vendor cannot import products + + } + + + @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.ProductListButtons, additionalData = Model }) +
+
+ +
+
+
+
+ + +
+
+ + + @await Html.PartialAsync("Table", new DataTablesModel + { + Name = "products-grid", + UrlRead = new DataUrl("ProductList", "CustomProduct", null), + SearchButtonId = "search-products", + Length = Model.PageSize, + LengthMenu = Model.AvailablePageSizes, + Filters = new List + { + new FilterParameter(nameof(Model.SearchProductName)), + new FilterParameter(nameof(Model.SearchCategoryId)), + new FilterParameter(nameof(Model.SearchIncludeSubCategories), typeof(bool)), + new FilterParameter(nameof(Model.SearchManufacturerId)), + new FilterParameter(nameof(Model.SearchStoreId)), + new FilterParameter(nameof(Model.SearchWarehouseId)), + new FilterParameter(nameof(Model.SearchVendorId)), + new FilterParameter(nameof(Model.SearchProductTypeId)), + new FilterParameter(nameof(Model.SearchPublishedId)) + }, + ColumnCollection = new List + { + new ColumnProperty(nameof(ProductModel.Id)) + { + IsMasterCheckBox = true, + Render = new RenderCheckBox("checkbox_products"), + ClassName = NopColumnClassDefaults.CenterAll, + Width = "50" + }, + new ColumnProperty(nameof(ProductModel.PictureThumbnailUrl)) + { + Title = T("Admin.Catalog.Products.Fields.PictureThumbnailUrl").Text, + Width = "100", + Render = new RenderPicture(width: 100) + }, + new ColumnProperty(nameof(ProductModel.Name)) + { + Title = T("Admin.Catalog.Products.Fields.Name").Text + }, + new ColumnProperty(nameof(ProductModel.Sku)) + { + Title = T("Admin.Catalog.Products.Fields.Sku").Text, + Width = "100" + }, + new ColumnProperty(nameof(ProductModel.FormattedPrice)) + { + Title = T("Admin.Catalog.Products.Fields.Price").Text + }, + new ColumnProperty(nameof(ProductModel.StockQuantityStr)) + { + Title = T("Admin.Catalog.Products.Fields.StockQuantity").Text + }, + new ColumnProperty(nameof(ProductModel.Published)) + { + Title = T("Admin.Catalog.Products.Fields.Published").Text, + Width = "80", + ClassName = NopColumnClassDefaults.CenterAll, + Render = new RenderBoolean() + }, + new ColumnProperty(nameof(ProductModel.Id)) + { + Title = T("Admin.Common.Edit").Text, + Width = "80", + ClassName = NopColumnClassDefaults.Button, + Render = new RenderButtonEdit(new DataUrl("~/Admin/Product/Edit")) + } + } + }) + + + + +
+
+
+
+
+
+ +
+} + + + + + @*import products form*@ + + + @*export selected (XML). We don't use GET approach because it's limited to 2K-4K chars and won't work for large number of entities*@ +
+ +
+ + + + + @*export selected (Excel). We don't use GET approach because it's limited to 2K-4K chars and won't work for large number of entities*@ +
+ +
+ + + \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs index 6104a13..afaeca5 100644 --- a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs +++ b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs @@ -68,7 +68,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin }; await _settingService.SaveSettingAsync(settings); await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Shipment", "EN"); - await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Shipment", "HU"); + await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Szállítmányok", "HU"); await base.InstallAsync(); } diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs index 7566a12..0cdde7f 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs @@ -78,9 +78,9 @@ public class PluginNopStartup : INopStartup services.AddScoped(); services.AddScoped(); - //services.AddScoped(); - //services.AddScoped(); - //services.AddScoped(); + services.AddScoped, OrderListModelExtended>(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs index b4a47d2..688b0e3 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs @@ -111,6 +111,16 @@ public class RouteProvider : IRouteProvider name: "Plugin.FruitBank.Admin.ManagementPage.GetPartners", pattern: "Admin/ManagementPage/GetPartners", defaults: new { controller = "ManagementPage", action = "GetPartners", area = AreaNames.ADMIN }); + + endpointRouteBuilder.MapControllerRoute( + name: "Plugin.FruitBank.Admin.Products.List", + pattern: "Admin/Product/List", + defaults: new { controller = "CustomProduct", action = "List", area = AreaNames.ADMIN }); + + endpointRouteBuilder.MapControllerRoute( + name: "Plugin.FruitBank.Admin.Products.ProductList", + pattern: "Admin/Product/ProductList", + defaults: new { controller = "CustomProduct", action = "ProductList", area = AreaNames.ADMIN }); } /// diff --git a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj index 41014e9..c76f8b8 100644 --- a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj +++ b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj @@ -37,6 +37,11 @@ PreserveNewest Always + + true + PreserveNewest + Always + true PreserveNewest @@ -149,6 +154,12 @@ Always + + Always + + + Always + Always From 479914dc8ad7546b2eb6684135c862f04628107c Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 13 Oct 2025 18:06:28 +0200 Subject: [PATCH 2/3] viecomponent logic --- .../Components/FileUploadGridComponent.cs | 2 +- .../Admin/Components/TestGridComponent.cs | 4 +- .../Controllers/CustomOrderController.cs | 4 +- .../FileManagerScriptsApiController.cs | 6 ++- .../Controllers/ManagementPageController.cs | 48 ++++++++++++------- .../Order/FileUploadGridComponent.cshtml | 42 +++++++++------- .../ShippingDocumentGridComponent.cshtml | 4 ++ .../Views/Order/TestGridComponent.cshtml | 2 +- .../Infrastructure/RouteProvider.cs | 5 ++ 9 files changed, 74 insertions(+), 43 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/FileUploadGridComponent.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/FileUploadGridComponent.cs index 6894a0d..820c413 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/FileUploadGridComponent.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/FileUploadGridComponent.cs @@ -10,7 +10,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components { // Here you can fetch data for this grid if needed // For demo, just pass the model - return View(model.ViewComponentName +".cshtml", model); + return View(model.ViewComponentLocation, model); } } } diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/TestGridComponent.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/TestGridComponent.cs index a7f5782..1e5d716 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/TestGridComponent.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/TestGridComponent.cs @@ -10,8 +10,8 @@ using System.Text.Json; namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components { - [ViewComponent(Name = "TestChildGrid")] - public class TestChildGridViewComponent : ViewComponent + [ViewComponent(Name = "TestGridComponent")] + public class TestGridComponent : ViewComponent { public IViewComponentResult Invoke(TestGridModel model) { diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs index 5b9c5c3..81e4028 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs @@ -65,8 +65,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers public async Task OrderList(OrderSearchModel searchModel) { //prepare model - //var orderListModel = await GetOrderListModelByFilter(searchModel); - var orderListModel = new OrderListModel(); + var orderListModel = await GetOrderListModelByFilter(searchModel); + //var orderListModel = new OrderListModel(); var valami = Json(orderListModel); Console.WriteLine(valami); diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs index ab40297..bf40f0f 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs @@ -1,6 +1,7 @@ using DevExtreme.AspNet.Mvc.FileManagement; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using System.Collections.Generic; namespace DevExtreme.NETCore.Demos.Controllers @@ -20,12 +21,15 @@ namespace DevExtreme.NETCore.Demos.Controllers [Route("api/file-manager-file-system", Name = "FileManagementFileSystemApi")] public object FileSystem(FileSystemCommand command, string arguments, int orderId) { + + string path = Request.Headers["TestHeader"]; + var valami = new List(); var config = new FileSystemConfiguration { Request = Request, - FileSystemProvider = new PhysicalFileSystemProvider(_webHostEnvironment.ContentRootPath + $"/wwwroot/uploads/orders/{orderId}"), + FileSystemProvider = new PhysicalFileSystemProvider(_webHostEnvironment.ContentRootPath + $"/wwwroot/uploads/orders/order{path}"), //uncomment the code below to enable file/folder management //AllowCopy = true, //AllowCreate = true, diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs index 971499d..c74551e 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs @@ -42,18 +42,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers var testGridModel2 = new TestGridModel(); testGridModel2.GridName = "Orders"; testGridModel2.ViewComponentName = "ShippingDocumentGridComponent"; + testGridModel2.ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml"; + testGridModel2.Configuration = new GridConfiguration(); + testGridModel2.Configuration.ShowChildGridsAsTabs = true; testGridModel2.ChildGrids = new List(); var childGrid1 = new TestGridModel { GridName = "TestGrid", ViewComponentName = "TestGridComponent", + ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml", ParentGridId = testGridModel2.Id, ChildGrids = new List() }; - testGridModel2.Configuration = new GridConfiguration(); - testGridModel2.Configuration.ShowChildGridsAsTabs = true; testGridModel2.ChildGrids.Add(childGrid1); + var childGrid2 = new TestGridModel + { + GridName = "Files", + ViewComponentName = "FileUploadGridComponent", + ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml", + ParentGridId = testGridModel2.Id, + ChildGrids = new List() + }; + testGridModel2.ChildGrids.Add(childGrid2); + testPageModel.Grids.Add(testGridModel2); @@ -72,7 +84,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers } [HttpPost] - public async Task LoadChildGrid([FromBody] LoadChildGridRequest request) + public IActionResult LoadChildGrid([FromBody] LoadChildGridRequest request) { // request.contextId is the actual row ID (data.Id from DevExtreme) // request.childModel is the full TestGridModel object @@ -84,9 +96,17 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers request.ChildModel.DataContext["contextId"] = request.ContextId; // Invoke the view component with the full model - return ViewComponent(request.ChildModel.ViewComponentLocation, new { model = request.ChildModel }); + return ViewComponent(request.ChildModel.ViewComponentName, request.ChildModel); } + // Request model for deserialization + public class LoadChildGridRequest + { + public int ContextId { get; set; } // The actual row ID from data.Id + public TestGridModel ChildModel { get; set; } // The full child grid model + } + + [HttpGet] public async Task GetShippings() { @@ -165,16 +185,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers } - - - // Request model for deserialization - public class LoadChildGridRequest - { - public int ContextId { get; set; } // The actual row ID from data.Id - public TestGridModel ChildModel { get; set; } // The full child grid model - } - - [HttpPost] [RequestSizeLimit(10485760)] // 10MB [RequestFormLimits(MultipartBodyLengthLimit = 10485760)] @@ -256,7 +266,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers } string analysisPrompt = "Extract the document identification number from this document, determine the type of the " + - "document from the available list, and return them as JSON: documentNumber, documentType. " + + "document IN ENGLISH from the available list, and return them as JSON: documentNumber, documentType. " + $"Available filetypes: {nameof(DocumentType.Invoice)}, {nameof(DocumentType.ShippingDocument)} , {nameof(DocumentType.OrderConfirmation)}, {nameof(DocumentType.Unknown)}" + "If you can't find information of any of these, return null value for that field."; @@ -266,10 +276,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers var extractedMetaData = ParseMetaDataAIResponse(metaAnalyzis); if (extractedMetaData.DocumentNumber != null) - + { dbFile.RawText = pdfText; - dbFile.FileExtension = "pdf"; - dbFile.FileName = extractedMetaData.DocumentNumber; + dbFile.FileExtension = "pdf"; + dbFile.FileName = extractedMetaData.DocumentNumber; + } + // - IF WE DON'T HAVE PARTNERID ALREADY: read partner information // (check if all 3 refers to the same partner) diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml index b721546..948f6ea 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml @@ -2,22 +2,30 @@ @using DevExtreme.AspNet.Mvc @{ - if (Model.DataContext["contextId"] != null) + + + if (Model.DataContext.ContainsKey("contextId")) { - var contextId = Model.DataContext["contextId"]; + -

contextId

+

@Model.DataContext["contextId"]

- }) + } // var gridId = $"dataGrid_{Guid.NewGuid():N}"; } +@{ + var contextId = Model.DataContext["contextId"]; + var fileManagerId = $"fileManager_{contextId}"; + var beforeAjaxSendFunctionName = $"beforeAjaxSend_{contextId}"; +} + +

@Model.DataContext["contextId"], @Model.GridName, @Model.ViewComponentName

- @Model.DataContext["contextId"], @Model.GridName, @Model.ViewComponentName - @*
+ @(Html.DevExtreme().FileUploader() .ID("shippingDocumentUploader-" + contextId) .Name("files") @@ -26,9 +34,9 @@ .UploadMode(FileUploadMode.UseForm) ) - <% if (data.PartnerId) { %> + - <% } %> + @(Html.DevExtreme().Button() .Text("Upload Files") .Type(ButtonType.Success) @@ -39,16 +47,16 @@

Selected Files

-
*@ +
- @* @(Html.DevExtreme().FileManager() - .ID("fileManager_"+ contextId) + @(Html.DevExtreme().FileManager() + .ID(fileManagerId) .FileSystemProvider(provider => provider.Remote() .Url(Url.RouteUrl("FileManagementFileSystemApi")) .BeforeAjaxSend(@ - function(e) { - e.ajaxSettings.data.orderId = data.Id; + function(arg) { + arg.headers.TestHeader = @Model.DataContext["contextId"]; } )) .Permissions(permissions => { @@ -56,11 +64,9 @@ permissions.Upload(true); }) .AllowedFileExtensions(new[] { ".pdf", ".jpg", ".jpeg" }) - ) *@ -
- - - + ) + + + diff --git a/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml index 221ea78..b144f6b 100644 --- a/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml @@ -1,4 +1,4 @@ -@* File: Plugins/Nop.Plugin.YourCompany.ProductAttributes/Views/ProductCustomAttributes.cshtml *@ + @model Nop.Plugin.Misc.FruitBankPlugin.Models.ProductAttributesModel