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..820c413 --- /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.ViewComponentLocation, 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..1e5d716 --- /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 = "TestGridComponent")] + public class TestGridComponent : ViewComponent + { + public IViewComponentResult Invoke(TestGridModel model) + { + return View(model.ViewComponentLocation, model); + } + } +} + diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml deleted file mode 100644 index 400c358..0000000 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.ShippingDocumentListModel -@using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models -@using FruitBank.Common.Entities; -@using DevExtreme.AspNet.Mvc -

Id: @Model.ShippingId

- -@(Html.DevExtreme().DataGrid() - .ID("documentsGrid") - .DataSource(Model.ShippingDocumentList) - .KeyExpr("Id") - .ShowBorders(true) - .Editing(editing => { - editing.Mode(GridEditMode.Row); - editing.AllowUpdating(true); - editing.AllowAdding(false); - editing.AllowDeleting(true); - }) - .Columns(c => { - c.AddFor(m => m.DocumentDate).Caption("Date").DataType(GridDataType.Date); - c.AddFor(m => m.SenderName).Caption("Sender"); - c.AddFor(m => m.InvoiceNumber).Caption("Invoice #"); - c.AddFor(m => m.TotalAmount).Caption("Amount").DataType(GridDataType.Number); - c.AddFor(m => m.ItemCount).Caption("ItemCount").DataType(GridDataType.Number); - }) -) \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs index 878a56e..dfb3dd9 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs @@ -9,6 +9,8 @@ using Nop.Core.Domain.Orders; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Factories; using Nop.Plugin.Misc.FruitBankPlugin.Models; +using Nop.Services.Common; +using Nop.Services.Messages; using Nop.Services.Orders; using Nop.Services.Security; using Nop.Web.Areas.Admin.Controllers; @@ -27,14 +29,18 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers private readonly CustomOrderModelFactory _orderModelFactory; private readonly ICustomOrderSignalREndpointServer _customOrderSignalREndpoint; private readonly IPermissionService _permissionService; + private readonly IGenericAttributeService _genericAttributeService; + private readonly INotificationService _notificationService; // ... other dependencies - public CustomOrderController(IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService) + public CustomOrderController(IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService, IGenericAttributeService genericAttributeService, INotificationService notificationService) { _orderService = orderService; _orderModelFactory = orderModelFactory as CustomOrderModelFactory; _customOrderSignalREndpoint = customOrderSignalREndpoint; _permissionService = permissionService; + _genericAttributeService = genericAttributeService; + _notificationService = notificationService; // ... initialize other deps } @@ -67,6 +73,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { //prepare model var orderListModel = await GetOrderListModelByFilter(searchModel); + //var orderListModel = new OrderListModel(); var valami = Json(orderListModel); Console.WriteLine(valami); @@ -103,6 +110,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers // return Json(model); //} + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task SaveOrderAttributes(OrderAttributesModel model) + { + if (!ModelState.IsValid) + { + // reload order page with errors + return RedirectToAction("Edit", "Order", new { id = model.OrderId }); + } + + var order = await _orderService.GetOrderByIdAsync(model.OrderId); + if (order == null) + return RedirectToAction("List", "Order"); + + // store attributes in GenericAttribute table + await _genericAttributeService.SaveAttributeAsync(order, nameof(OrderModelExtended.IsMeasurable), model.IsMeasurable); + await _genericAttributeService.SaveAttributeAsync(order, nameof(IOrderDto.DateOfReceipt), model.DateOfReceipt); + + _notificationService.SuccessNotification("Custom attributes saved successfully."); + + return RedirectToAction("Edit", "Order", new { id = model.OrderId }); + } + } } 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..bf40f0f --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerScriptsApiController.cs @@ -0,0 +1,48 @@ +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 +{ + 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) + { + + string path = Request.Headers["TestHeader"]; + + var valami = new List(); + + var config = new FileSystemConfiguration + { + Request = Request, + FileSystemProvider = new PhysicalFileSystemProvider(_webHostEnvironment.ContentRootPath + $"/wwwroot/uploads/orders/order{path}"), + //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..dbfaf58 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs @@ -24,12 +24,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers private readonly IPermissionService _permissionService; protected readonly FruitBankDbContext _dbContext; protected readonly AICalculationService _aiCalculationService; + protected readonly OpenAIApiService _openAIApiService; - public ManagementPageController(IPermissionService permissionService, FruitBankDbContext fruitBankDbContext, AICalculationService aiCalculationService) + public ManagementPageController(IPermissionService permissionService, FruitBankDbContext fruitBankDbContext, AICalculationService aiCalculationService, OpenAIApiService openAIApiService) { _permissionService = permissionService; _dbContext = fruitBankDbContext; _aiCalculationService = aiCalculationService; + _openAIApiService = openAIApiService; } public async Task Test() @@ -42,6 +44,31 @@ 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.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); var testGridModel = new TestGridModel(); @@ -58,6 +85,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/Test.cshtml", testPageModel); } + [HttpPost] + public IActionResult 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.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() { @@ -135,11 +186,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return Json(model); } + [HttpPost] [RequestSizeLimit(10485760)] // 10MB [RequestFormLimits(MultipartBodyLengthLimit = 10485760)] public async Task UploadFile(List files, int shippingDocumentId, int? partnerId) { + + var shippingDocument = await _dbContext.ShippingDocuments.GetByIdAsync(shippingDocumentId); //checks // - files exist if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) @@ -159,8 +213,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers } var filesList = new List(); - var shippingDocumentToFileList = new List(); - + //iteratation 1: iterate documents to determine their type by AI @@ -168,16 +221,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers foreach (var file in files) { - var fileName = file.FileName; var fileSize = file.Length; var dbFile = new Files(); string pdfText = ""; - Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}"); + Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}, content type: {file.ContentType}"); - if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) - return Json(new { success = false, errorMessage = "Only PDF files are allowed" }); + if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) && !file.ContentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase)) + return Json(new { success = false, errorMessage = "Only PDF or jpg files are allowed" }); // Validate file size (max 20MB) if (file.Length > 20 * 1024 * 1024) @@ -185,38 +237,91 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers // - get text extracted from pdf // Validate file type (PDF only) - if (file.Length > 0 && file.ContentType == "application/pdf") + //if (file.Length > 0 && file.ContentType == "application/pdf") + if (file.Length > 0) { - - try - { - // Open the PDF from the IFormFile's stream directly in memory - using (var stream = file.OpenReadStream()) - using (var pdf = UglyToad.PdfPig.PdfDocument.Open(stream)) + if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)){ + try { - // Now you can analyze the PDF content - - foreach (var page in pdf.GetPages()) + // Open the PDF from the IFormFile's stream directly in memory + using (var stream = file.OpenReadStream()) + using (var pdf = UglyToad.PdfPig.PdfDocument.Open(stream)) { - // Extract text from each page - pdfText += ContentOrderTextExtractor.GetText(page); + // Now you can analyze the PDF content + + foreach (var page in pdf.GetPages()) + { + // Extract text from each page + pdfText += ContentOrderTextExtractor.GetText(page); + + } + //still nothing? let's send it to AI + if (string.IsNullOrWhiteSpace(pdfText)) + { + try + { + // ✅ Use the service we implemented earlier + pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this PDF."); + } + catch (Exception aiEx) + { + Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}"); + return StatusCode(500, $"Failed to process PDF: {aiEx.Message}"); + } + } + + // For demonstration, let's just log the extracted text + Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}"); + } - - // For demonstration, let's just log the extracted text - Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}"); - + } + catch (Exception ex) + { + // Handle potential exceptions during PDF processing + Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}"); + return StatusCode(500, $"Error processing PDF file: {ex.Message}"); } } - catch (Exception ex) + else { - // Handle potential exceptions during PDF processing - Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}"); - return StatusCode(500, $"Error processing PDF file: {ex.Message}"); + try + { + // Open the PDF from the IFormFile's stream directly in memory + using (var stream = file.OpenReadStream()) + { + + try + { + // ✅ Use the service we implemented earlier + pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this PDF."); + } + catch (Exception aiEx) + { + Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}"); + return StatusCode(500, $"Failed to process PDF: {aiEx.Message}"); + } + + // For demonstration, let's just log the extracted text + Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}"); + + } + } + + catch (Exception ex) + { + // Handle potential exceptions during PDF processing + Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}"); + return StatusCode(500, $"Error processing PDF file: {ex.Message}"); + } } + //we should have some kind of text now + Console.WriteLine(pdfText); } + + 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."; @@ -226,11 +331,22 @@ 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; + } + await _dbContext.Files.InsertAsync(dbFile); + filesList.Add(dbFile); + ShippingDocumentToFiles shippingDocumentToFiles = new ShippingDocumentToFiles + { + ShippingDocumentId = shippingDocumentId, + FilesId = dbFile.Id + }; + + + await _dbContext.ShippingDocumentToFiles.InsertAsync(shippingDocumentToFiles); // - IF WE DON'T HAVE PARTNERID ALREADY: read partner information // (check if all 3 refers to the same partner) // save partner information to partners table { Id, Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street } @@ -254,7 +370,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..ad331a6 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml @@ -0,0 +1,112 @@ +@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel +@using DevExtreme.AspNet.Mvc + + +@{ + var contextId = Model.DataContext["contextId"]; + var fileManagerId = $"fileManager_{contextId}"; + var beforeAjaxSendFunctionName = $"beforeAjaxSend_{contextId}"; +} + + + +
+
+ +
+ @(Html.DevExtreme().FileUploader() + .ID("shippingDocumentUploader-" + contextId) + .Name("files") + .Multiple(true) + .Accept("application/pdf") + .UploadMode(FileUploadMode.UseForm) + ) + + + + + @(Html.DevExtreme().Button() + .Text("Upload Files") + .Type(ButtonType.Success) + .UseSubmitBehavior(true) + ) +
+
+
+

Selected Files

+
+
+
+
+ @(Html.DevExtreme().FileManager() + .ID(fileManagerId) + .FileSystemProvider(provider => provider.Remote() + .Url(Url.RouteUrl("FileManagementFileSystemApi")) + .BeforeAjaxSend(@ + function(arg) { + arg.headers.TestHeader = @Model.DataContext["contextId"]; + } + )) + .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..0540362 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml @@ -1,6 +1,7 @@ @model OrderSearchModel @inject IStoreService storeService +@using FruitBank.Common.Interfaces @using Nop.Plugin.Misc.FruitBankPlugin.Models @using Nop.Services.Stores @using Nop.Web.Areas.Admin.Components @@ -290,9 +291,7 @@
-
-

RTTTTTTTTTT

-
+
@@ -342,11 +341,18 @@ }; gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable)) { - Title = "Needs Measurement", + Title = T("Admin.Orders.Fields.ToBeMeasured").Text, Width = "100", - Render = new RenderCustom("renderColumnNeedsMeasurement"), + Render = new RenderCustom("renderColumnIsMeasurable"), ClassName = NopColumnClassDefaults.CenterAll }); + gridModel.ColumnCollection.Add(new ColumnProperty(nameof(IOrderDto.DateOfReceipt)) + { + Title = T("Admin.Orders.Fields.PickupDate").Text, + Width = "100", + Render = new RenderCustom("renderColumnPickupDateAndTime"), + ClassName = NopColumnClassDefaults.CenterAll + }); //a vendor does not have access to this functionality if (!Model.IsLoggedInAsVendor) @@ -441,11 +447,16 @@ return `${textRenderer(row.CustomerFullName)}
${data} `; } - function renderColumnNeedsMeasurement(data, type, row, meta) { + function renderColumnIsMeasurable(data, type, row, meta) { if(data === true) { - return 'Yes'; + return 'Yes'; } - return 'No'; + return 'No'; + } + + function renderColumnPickupDateAndTime(data, type, row, meta) { + + return `${data}`; } $(function() { 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..f3a41a6 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml @@ -1,10 +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.AntiForgeryToken()
@( @@ -27,23 +23,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 +54,146 @@ ); }); }) - .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..5713410 --- /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/Components/OrderAttributesViewComponent.cs b/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs new file mode 100644 index 0000000..6cc2ddc --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs @@ -0,0 +1,55 @@ +// File: Plugins/YourCompany.ProductAttributes/Components/ProductAttributesViewComponent.cs + +using FruitBank.Common.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Nop.Core; +using Nop.Core.Domain.Catalog; +using Nop.Core.Domain.Orders; +using Nop.Plugin.Misc.FruitBankPlugin.Models; +using Nop.Plugin.Misc.FruitBankPlugin.Services; +using Nop.Services.Common; +using Nop.Web.Areas.Admin.Models.Catalog; +using Nop.Web.Areas.Admin.Models.Orders; +using Nop.Web.Framework.Components; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Components +{ + [ViewComponent(Name = "OrderAttributes")] + public class OrderAttributesViewComponent : NopViewComponent + { + private readonly FruitBankAttributeService _fruitBankAttributeService; + private readonly IWorkContext _workContext; + private readonly IStoreContext _storeContext; + + public OrderAttributesViewComponent(FruitBankAttributeService fruitBankAttributeService, IWorkContext workContext, IStoreContext storeContext) + { + _workContext = workContext; + _storeContext = storeContext; + _fruitBankAttributeService = fruitBankAttributeService; + } + + public async Task InvokeAsync(string widgetZone, object additionalData) + { + if (additionalData is not OrderModel orderModel) return Content(""); + + var model = new OrderAttributesModel { OrderId = orderModel.Id }; + + if (model.OrderId > 0) + { + var orderPickupAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync(model.OrderId, nameof(IOrderDto.DateOfReceipt)); + var orderMeasurableAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync(model.OrderId, nameof(OrderModelExtended.IsMeasurable)); + + model.IsMeasurable = orderMeasurableAttributeValue; + if(orderPickupAttributeValue.HasValue && orderPickupAttributeValue.Value != DateTime.MinValue) + { + model.DateOfReceipt = orderPickupAttributeValue; + } + else + { + model.DateOfReceipt = null; + } + } + return View("~/Plugins/Misc.FruitBankPlugin/Views/OrderAttributes.cshtml", model); + } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs index 7ddf2ad..4e4c81e 100644 --- a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs +++ b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs @@ -1,4 +1,5 @@ using AyCode.Core.Extensions; +using FruitBank.Common.Interfaces; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.TagHelpers; @@ -43,6 +44,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories { private FruitBankDbContext _ctx; private readonly IOrderMeasurementService _orderMeasurementService; + private readonly IGenericAttributeService _genericAttributeService; public CustomOrderModelFactory( FruitBankDbContext ctx, @@ -91,36 +93,37 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories OrderSettings orderSettings, ShippingSettings shippingSettings, IUrlRecordService urlRecordService, - TaxSettings taxSettings - ) : base(addressSettings, - catalogSettings, - currencySettings, - actionContextAccessor, - addressModelFactory, - addressService, - affiliateService, - baseAdminModelFactory, - countryService, - currencyService, - customerService, - dateTimeHelper, - discountService, - downloadService, - encryptionService, - giftCardService, - localizationService, - measureService, - orderProcessingService, - orderReportService, - orderService, - paymentPluginManager, - paymentService, - pictureService, - priceCalculationService, - priceFormatter, - productAttributeService, - productService, - returnRequestService, + TaxSettings taxSettings, + IGenericAttributeService genericAttributeService + ) : base(addressSettings, + catalogSettings, + currencySettings, + actionContextAccessor, + addressModelFactory, + addressService, + affiliateService, + baseAdminModelFactory, + countryService, + currencyService, + customerService, + dateTimeHelper, + discountService, + downloadService, + encryptionService, + giftCardService, + localizationService, + measureService, + orderProcessingService, + orderReportService, + orderService, + paymentPluginManager, + paymentService, + pictureService, + priceCalculationService, + priceFormatter, + productAttributeService, + productService, + returnRequestService, rewardPointService, settingService, shipmentService, @@ -141,6 +144,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories { _ctx = ctx; _orderMeasurementService = orderMeasurementService; + _genericAttributeService = genericAttributeService; } public override async Task PrepareOrderSearchModelAsync(OrderSearchModel searchModel) @@ -173,7 +177,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories } public override async Task PrepareOrderListModelAsync(OrderSearchModel searchModel) - =>await base.PrepareOrderListModelAsync(searchModel); + => await base.PrepareOrderListModelAsync(searchModel); public async Task PrepareOrderListModelExtendedAsync(OrderSearchModel searchModel) { @@ -188,6 +192,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories PropertyHelper.CopyPublicValueTypeProperties(orderModel, orderModelExtended); orderModelExtended.IsMeasurable = orderDtosById[orderModel.Id].IsMeasurable;// await ShouldMarkAsNeedsMeasurementAsync(orderModel); + orderModelExtended.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(orderModel); + orderModelExtended.DateOfReceipt = await GetPickupDateTimeAsync(orderModel); Console.WriteLine(orderModelExtended.Id); extendedRows.Add(orderModelExtended); @@ -216,7 +222,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories return await Task.FromResult(false); } - + private async Task GetPickupDateTimeAsync(OrderModel order) + { + DateTime? dateTime = DateTime.MinValue; + var fullOrder = await _orderService.GetOrderByIdAsync(order.Id); + if (fullOrder != null) + { + dateTime = await _genericAttributeService.GetAttributeAsync(fullOrder, nameof(IOrderDto.DateOfReceipt)); + if(dateTime == DateTime.MinValue || !dateTime.HasValue) + { + dateTime = null; + } + } + return dateTime; + } } } diff --git a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs index 6104a13..4d5f0dd 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(); } @@ -84,7 +84,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin public Task> GetWidgetZonesAsync() { - return Task.FromResult>(new List { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock }); + return Task.FromResult>(new List { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock, AdminWidgetZones.OrderDetailsBlock }); } //public string GetWidgetViewComponentName(string widgetZone) @@ -138,6 +138,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin { return zones.Any(widgetZone.Equals) ? typeof(ProductAttributesViewComponent) : null; } + + else if (widgetZone == AdminWidgetZones.OrderDetailsBlock) + { + return zones.Any(widgetZone.Equals) ? typeof(OrderAttributesViewComponent) : null; + } } return null; diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs index b4a47d2..c37686b 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs @@ -111,6 +111,26 @@ 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.ManagementPage.LoadChildGrid", + pattern: "Admin/ManagementPage/LoadChildGrid", + defaults: new { controller = "ManagementPage", action = "LoadChildGrid", 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 }); + + endpointRouteBuilder.MapControllerRoute( + name: "Plugin.FruitBank.Admin.Orders.SaveOrderAttributes", + pattern: "Admin/CustomOrder/SaveOrderAttributes", + defaults: new { controller = "CustomOrder", action = "SaveOrderAttributes", area = AreaNames.ADMIN }); } /// diff --git a/Nop.Plugin.Misc.AIPlugin/Models/OrderAttributesModel.cs b/Nop.Plugin.Misc.AIPlugin/Models/OrderAttributesModel.cs new file mode 100644 index 0000000..080e9f6 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Models/OrderAttributesModel.cs @@ -0,0 +1,18 @@ +using FruitBank.Common.Interfaces; +using Nop.Web.Framework.Models; +using Nop.Web.Framework.Mvc.ModelBinding; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Models +{ + public record OrderAttributesModel : BaseNopModel, IMeasurable + { + public int OrderId { get; set; } + + [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.IsMeasurable")] + public bool IsMeasurable { get; set; } + + [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.DateOfReceipt")] + public DateTime? DateOfReceipt { get; set; } + + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs b/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs index 5d6c936..8687e20 100644 --- a/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs +++ b/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs @@ -5,6 +5,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models public partial record OrderModelExtended : OrderModel { public bool IsMeasurable { get; set; } + public DateTime? DateOfReceipt { get; set; } } } diff --git a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj index 41014e9..ac55a51 100644 --- a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj +++ b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj @@ -7,6 +7,9 @@ true true + + + @@ -28,7 +31,10 @@ + + + @@ -37,6 +43,11 @@ PreserveNewest Always + + true + PreserveNewest + Always + true PreserveNewest @@ -146,7 +157,10 @@ - + + Always + + Always @@ -605,6 +619,9 @@ Always + + Always + Always @@ -620,6 +637,10 @@ + + + + diff --git a/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs b/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs index e90d7c1..f5e65f8 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs @@ -1,14 +1,6 @@ -using Azure; -using Nop.Core; +using Nop.Core; using Nop.Core.Domain.Customers; -using Nop.Core.Domain.Stores; using Nop.Plugin.Misc.FruitBankPlugin.Helpers; -using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Nop.Plugin.Misc.FruitBankPlugin.Services { @@ -37,16 +29,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return response; } - //public async Task GetOpenAIPDFAnalyzisFromText(string pdfText, string userQuestion) - //{ - // string systemMessage = $"You are a helpful assistant of a webshop, you work in the administration area, with the ADMIN user. The ADMIN user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}"; - // var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion); - - // var fixedResponse = TextHelper.FixJsonWithoutAI(response); - - // return fixedResponse; - //} - public async Task GetOpenAIPDFAnalysisFromText(string pdfText, string userQuestion) { string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}"; @@ -57,15 +39,5 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return fixedResponse; } - //public async Task GetOpenAIPartnerInfoFromText(string pdfText, string userQuestion) - //{ - // string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}"; - // var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion); - - // var fixedResponse = TextHelper.FixJsonWithoutAI(response); - - // return fixedResponse; - //} - } } diff --git a/Nop.Plugin.Misc.AIPlugin/Services/InnvoiceApiService.cs b/Nop.Plugin.Misc.AIPlugin/Services/InnvoiceApiService.cs new file mode 100644 index 0000000..ad3a08b --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Services/InnvoiceApiService.cs @@ -0,0 +1,496 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Xml.Linq; +using System.Linq; +using System.Text; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Services +{ + /// + /// Service for interacting with InnVoice Invoice API + /// API Documentation: https://help.innvoice.hu/hc/hu/articles/360003142839 + /// + public class InnVoiceApiService + { + private readonly HttpClient _httpClient; + private readonly string _companyName; + private readonly string _username; + private readonly string _password; + private readonly string _baseUrl; + + public InnVoiceApiService(string companyName, string username, string password, string baseUrl = "https://api.innvoice.hu") + { + _companyName = companyName ?? throw new ArgumentNullException(nameof(companyName)); + _username = username ?? throw new ArgumentNullException(nameof(username)); + _password = password ?? throw new ArgumentNullException(nameof(password)); + _baseUrl = baseUrl.TrimEnd('/'); + + _httpClient = new HttpClient(); + SetupAuthentication(); + } + + private void SetupAuthentication() + { + var authToken = Convert.ToBase64String( + System.Text.Encoding.ASCII.GetBytes($"{_username}:{_password}") + ); + _httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken); + } + + /// + /// Get all invoices + /// Rate limit: 20 times per hour without ID parameter + /// + public async Task> GetAllInvoicesAsync() + { + var url = $"{_baseUrl}/{_companyName}/invoice"; + return await GetInvoicesFromUrlAsync(url); + } + + /// + /// Get invoice by internal table ID + /// + public async Task GetInvoiceByIdAsync(int tableId) + { + var url = $"{_baseUrl}/{_companyName}/invoice/id/{tableId}"; + var invoices = await GetInvoicesFromUrlAsync(url); + return invoices.Count > 0 ? invoices[0] : null; + } + + /// + /// Get invoice by invoice number + /// + public async Task> GetInvoiceByNumberAsync(string invoiceNumber) + { + var url = $"{_baseUrl}/{_companyName}/invoice/invoicenumber/{Uri.EscapeDataString(invoiceNumber)}"; + return await GetInvoicesFromUrlAsync(url); + } + + /// + /// Get invoices by creation date + /// Format: YYYYMMDD + /// + public async Task> GetInvoicesByCreationDateAsync(DateTime date) + { + var dateStr = date.ToString("yyyyMMdd"); + var url = $"{_baseUrl}/{_companyName}/invoice/created/{dateStr}"; + return await GetInvoicesFromUrlAsync(url); + } + + /// + /// Get invoices by fulfillment date + /// Format: YYYYMMDD + /// + public async Task> GetInvoicesByFulfillmentDateAsync(DateTime date) + { + var dateStr = date.ToString("yyyyMMdd"); + var url = $"{_baseUrl}/{_companyName}/invoice/fulfillment/{dateStr}"; + return await GetInvoicesFromUrlAsync(url); + } + + /// + /// Get invoices by due date + /// Format: YYYYMMDD + /// + public async Task> GetInvoicesByDueDateAsync(DateTime date) + { + var dateStr = date.ToString("yyyyMMdd"); + var url = $"{_baseUrl}/{_companyName}/invoice/duedate/{dateStr}"; + return await GetInvoicesFromUrlAsync(url); + } + + /// + /// Get invoices by payment date + /// Format: YYYYMMDD + /// + public async Task> GetInvoicesByPaymentDateAsync(DateTime date) + { + var dateStr = date.ToString("yyyyMMdd"); + var url = $"{_baseUrl}/{_companyName}/invoice/paymentdate/{dateStr}"; + return await GetInvoicesFromUrlAsync(url); + } + + /// + /// Get invoices by customer tax number + /// + public async Task> GetInvoicesByCustomerTaxNumberAsync(string taxNumber) + { + var url = $"{_baseUrl}/{_companyName}/invoice/taxnumber/{Uri.EscapeDataString(taxNumber)}"; + return await GetInvoicesFromUrlAsync(url); + } + + /// + /// Get invoices modified since a specific timestamp + /// Format: YYYYMMDDHHmmss (year, month, day, hour, minute, second) + /// Recommended for tracking changes every 10 minutes + /// Rate limit: Full queries or queries older than current month limited to 10 times per 30 days + /// Recommended: Only use current month dates + /// + public async Task> GetInvoicesByUpdateTimeAsync(DateTime updateTime) + { + var timeStr = updateTime.ToString("yyyyMMddHHmmss"); + var url = $"{_baseUrl}/{_companyName}/invoice/updatedtime/{timeStr}"; + return await GetInvoicesFromUrlAsync(url); + } + + private async Task> GetInvoicesFromUrlAsync(string url) + { + try + { + var response = await _httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + var invoices = JsonSerializer.Deserialize>(content, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return invoices ?? new List(); + } + catch (HttpRequestException ex) + { + throw new InnVoiceApiException($"Error calling InnVoice API: {ex.Message}", ex); + } + catch (JsonException ex) + { + throw new InnVoiceApiException($"Error parsing API response: {ex.Message}", ex); + } + } + } + + // Models + public class Invoice + { + [JsonPropertyName("TABLE_ID")] + public int TableId { get; set; } + + [JsonPropertyName("InvoiceNumber")] + public string InvoiceNumber { get; set; } + + [JsonPropertyName("Created")] + public string Created { get; set; } + + [JsonPropertyName("Fulfillment")] + public string Fulfillment { get; set; } + + [JsonPropertyName("DueDate")] + public string DueDate { get; set; } + + [JsonPropertyName("PaymentDate")] + public string PaymentDate { get; set; } + + [JsonPropertyName("CustomerName")] + public string CustomerName { get; set; } + + [JsonPropertyName("CustomerTaxNumber")] + public string CustomerTaxNumber { get; set; } + + [JsonPropertyName("CustomerAddress")] + public string CustomerAddress { get; set; } + + [JsonPropertyName("TotalNet")] + public decimal TotalNet { get; set; } + + [JsonPropertyName("TotalGross")] + public decimal TotalGross { get; set; } + + [JsonPropertyName("Currency")] + public string Currency { get; set; } + + [JsonPropertyName("Status")] + public string Status { get; set; } + + [JsonPropertyName("InvoiceType")] + public string InvoiceType { get; set; } + + [JsonPropertyName("PaymentMethod")] + public string PaymentMethod { get; set; } + + // Add more properties as needed based on actual API response + } + + public class InnVoiceApiException : Exception + { + public InnVoiceApiException(string message) : base(message) { } + public InnVoiceApiException(string message, Exception innerException) : base(message, innerException) { } + } + + // Invoice Creation Models + public class InvoiceCreateRequest + { + public int VevoID { get; set; } = 0; + public string VevoNev { get; set; } + public string VevoIrsz { get; set; } + public string VevoOrszag { get; set; } + public string VevoTelep { get; set; } + public string VevoUtcaHsz { get; set; } + public string VevoEPNev { get; set; } + public string VevoEPKod { get; set; } + public string SzallNev { get; set; } + public string SzallIrsz { get; set; } + public string SzallTelep { get; set; } + public string SzallUtcaHsz { get; set; } + public string SzallOrszag { get; set; } + public int SzamlatombID { get; set; } + public DateTime SzamlaKelte { get; set; } + public DateTime TeljesitesKelte { get; set; } + public DateTime Hatarido { get; set; } + public string Devizanem { get; set; } + public string FizetesiMod { get; set; } + public string Megjegyzes { get; set; } + public string Nyelv1 { get; set; } + public string Nyelv2 { get; set; } + public decimal? Arfolyam { get; set; } + public string ArfolyamDeviza { get; set; } + public bool Fizetve { get; set; } + public bool Eszamla { get; set; } + public string VevoAdoszam { get; set; } + public string VevoCsAdoszam { get; set; } + public string Telefon { get; set; } + public string Email { get; set; } + public string MegrendelesSzamStr { get; set; } + public string MegrendelesIdopontStr { get; set; } + public bool Felretett { get; set; } + public bool Proforma { get; set; } + public bool AutomatikusAr { get; set; } + public bool Eloleg { get; set; } + public bool Sendmail { get; set; } + public string MailSubject { get; set; } + public string MailBody { get; set; } + public string Eredetiszamla { get; set; } + + public List Items { get; set; } = new List(); + + public void AddItem(InvoiceItem item) + { + Items.Add(item); + } + + public string ToXml() + { + var invoices = new XElement("invoices"); + var invoice = new XElement("invoice"); + + if (VevoID > 0) + invoice.Add(new XElement("VevoID", new XCData(VevoID.ToString()))); + + invoice.Add(new XElement("VevoNev", new XCData(VevoNev ?? ""))); + invoice.Add(new XElement("VevoIrsz", new XCData(VevoIrsz ?? ""))); + invoice.Add(new XElement("VevoTelep", new XCData(VevoTelep ?? ""))); + invoice.Add(new XElement("VevoOrszag", new XCData(VevoOrszag ?? ""))); + invoice.Add(new XElement("VevoUtcaHsz", new XCData(VevoUtcaHsz ?? ""))); + + if (!string.IsNullOrEmpty(VevoEPNev)) + invoice.Add(new XElement("VevoEPNev", new XCData(VevoEPNev))); + if (!string.IsNullOrEmpty(VevoEPKod)) + invoice.Add(new XElement("VevoEPKod", new XCData(VevoEPKod))); + if (!string.IsNullOrEmpty(SzallNev)) + invoice.Add(new XElement("SzallNev", new XCData(SzallNev))); + if (!string.IsNullOrEmpty(SzallIrsz)) + invoice.Add(new XElement("SzallIrsz", new XCData(SzallIrsz))); + if (!string.IsNullOrEmpty(SzallTelep)) + invoice.Add(new XElement("SzallTelep", new XCData(SzallTelep))); + if (!string.IsNullOrEmpty(SzallUtcaHsz)) + invoice.Add(new XElement("SzallUtcaHsz", new XCData(SzallUtcaHsz))); + if (!string.IsNullOrEmpty(SzallOrszag)) + invoice.Add(new XElement("SzallOrszag", new XCData(SzallOrszag))); + + invoice.Add(new XElement("SzamlatombID", new XCData(SzamlatombID.ToString()))); + invoice.Add(new XElement("SzamlaKelte", new XCData(SzamlaKelte.ToString("yyyy.MM.dd.")))); + invoice.Add(new XElement("TeljesitesKelte", new XCData(TeljesitesKelte.ToString("yyyy.MM.dd.")))); + invoice.Add(new XElement("Hatarido", new XCData(Hatarido.ToString("yyyy.MM.dd.")))); + invoice.Add(new XElement("Devizanem", new XCData(Devizanem ?? ""))); + invoice.Add(new XElement("FizetesiMod", new XCData(FizetesiMod ?? ""))); + + if (!string.IsNullOrEmpty(Megjegyzes)) + invoice.Add(new XElement("Megjegyzes", new XCData(Megjegyzes))); + if (!string.IsNullOrEmpty(Nyelv1)) + invoice.Add(new XElement("Nyelv1", new XCData(Nyelv1))); + if (!string.IsNullOrEmpty(Nyelv2)) + invoice.Add(new XElement("Nyelv2", new XCData(Nyelv2))); + if (Arfolyam.HasValue) + invoice.Add(new XElement("Arfolyam", new XCData(Arfolyam.Value.ToString()))); + if (!string.IsNullOrEmpty(ArfolyamDeviza)) + invoice.Add(new XElement("ArfolyamDeviza", new XCData(ArfolyamDeviza))); + + invoice.Add(new XElement("Fizetve", Fizetve ? "1" : "0")); + invoice.Add(new XElement("Eszamla", Eszamla ? "1" : "0")); + + if (!string.IsNullOrEmpty(VevoAdoszam)) + invoice.Add(new XElement("VevoAdoszam", new XCData(VevoAdoszam))); + if (!string.IsNullOrEmpty(VevoCsAdoszam)) + invoice.Add(new XElement("VevoCsAdoszam", new XCData(VevoCsAdoszam))); + if (!string.IsNullOrEmpty(Telefon)) + invoice.Add(new XElement("Telefon", new XCData(Telefon))); + if (!string.IsNullOrEmpty(Email)) + invoice.Add(new XElement("Email", new XCData(Email))); + if (!string.IsNullOrEmpty(MegrendelesSzamStr)) + invoice.Add(new XElement("MegrendelesSzamStr", new XCData(MegrendelesSzamStr))); + if (!string.IsNullOrEmpty(MegrendelesIdopontStr)) + invoice.Add(new XElement("MegrendelesIdopontStr", new XCData(MegrendelesIdopontStr))); + + invoice.Add(new XElement("Felretett", Felretett ? "1" : "0")); + invoice.Add(new XElement("Proforma", Proforma ? "1" : "0")); + + if (AutomatikusAr) + invoice.Add(new XElement("AutomatikusAr", "1")); + if (Eloleg) + invoice.Add(new XElement("Eloleg", "1")); + if (Sendmail) + { + invoice.Add(new XElement("Sendmail", "1")); + if (!string.IsNullOrEmpty(MailSubject)) + invoice.Add(new XElement("MailSubject", new XCData(MailSubject))); + if (!string.IsNullOrEmpty(MailBody)) + invoice.Add(new XElement("MailBody", new XCData(MailBody))); + } + if (!string.IsNullOrEmpty(Eredetiszamla)) + invoice.Add(new XElement("Eredetiszamla", new XCData(Eredetiszamla))); + + // Add items + foreach (var item in Items) + { + var tetel = new XElement("tetel"); + tetel.Add(new XElement("TetelNev", new XCData(item.TetelNev ?? ""))); + tetel.Add(new XElement("AfaSzoveg", item.AfaSzoveg ?? "")); + tetel.Add(new XElement("Brutto", item.Brutto ? "1" : "0")); + tetel.Add(new XElement("EgysegAr", item.EgysegAr.ToString())); + tetel.Add(new XElement("Mennyiseg", item.Mennyiseg.ToString())); + tetel.Add(new XElement("MennyisegEgyseg", new XCData(item.MennyisegEgyseg ?? ""))); + + if (item.KedvezmenyOsszeg.HasValue) + tetel.Add(new XElement("KedvezmenyOsszeg", item.KedvezmenyOsszeg.Value.ToString())); + if (item.TermekID.HasValue) + tetel.Add(new XElement("TermekID", item.TermekID.Value.ToString())); + if (!string.IsNullOrEmpty(item.Megjegyzes)) + tetel.Add(new XElement("Megjegyzes", new XCData(item.Megjegyzes))); + if (!string.IsNullOrEmpty(item.CikkSzam)) + tetel.Add(new XElement("CikkSzam", new XCData(item.CikkSzam))); + if (!string.IsNullOrEmpty(item.VTSZSZJ)) + tetel.Add(new XElement("VTSZSZJ", new XCData(item.VTSZSZJ))); + if (item.ElolegSzamlaTABLE_ID.HasValue) + tetel.Add(new XElement("ElolegSzamlaTABLE_ID", item.ElolegSzamlaTABLE_ID.Value.ToString())); + if (!string.IsNullOrEmpty(item.ElolegSzamlaSorszam)) + tetel.Add(new XElement("ElolegSzamlaSorszam", new XCData(item.ElolegSzamlaSorszam))); + + invoice.Add(tetel); + } + + invoices.Add(invoice); + return new XDeclaration("1.0", "UTF-8", null).ToString() + "\n" + invoices.ToString(); + } + } + + public class InvoiceItem + { + public string TetelNev { get; set; } + public string AfaSzoveg { get; set; } + public bool Brutto { get; set; } + public decimal EgysegAr { get; set; } + public decimal Mennyiseg { get; set; } + public string MennyisegEgyseg { get; set; } + public decimal? KedvezmenyOsszeg { get; set; } + public int? TermekID { get; set; } + public string Megjegyzes { get; set; } + public string CikkSzam { get; set; } + public string VTSZSZJ { get; set; } + public int? ElolegSzamlaTABLE_ID { get; set; } + public string ElolegSzamlaSorszam { get; set; } + } + + public class InvoiceCreateResponse + { + public string ErrorCode { get; set; } + public string Message { get; set; } + public int? TableId { get; set; } + public int? VevoID { get; set; } + public string TechId { get; set; } + public string Sorszam { get; set; } + public string PrintUrl { get; set; } + + public bool IsSuccess => ErrorCode == "200"; + + public static InvoiceCreateResponse FromXml(string xml) + { + var doc = XDocument.Parse(xml); + var invoice = doc.Descendants("invoice").FirstOrDefault(); + + if (invoice == null) + { + throw new InnVoiceApiException("Invalid XML response format"); + } + + return new InvoiceCreateResponse + { + ErrorCode = invoice.Element("error")?.Value, + Message = invoice.Element("message")?.Value?.Trim(), + TableId = int.TryParse(invoice.Element("TABLE_ID")?.Value?.Trim(), out var tid) ? tid : (int?)null, + VevoID = int.TryParse(invoice.Element("VevoID")?.Value?.Trim(), out var vid) ? vid : (int?)null, + TechId = invoice.Element("techid")?.Value?.Trim(), + Sorszam = invoice.Element("Sorszam")?.Value?.Trim(), + PrintUrl = invoice.Element("PrintUrl")?.Value?.Trim() + }; + } + } + + /// + /// Create a new invoice + /// + public async Task CreateInvoiceAsync(InvoiceCreateRequest request) + { + var url = $"{_baseUrl}/{_companyName}/invoice"; + + var xml = request.ToXml(); + var content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("data", xml) + }); + + try + { + var response = await _httpClient.PostAsync(url, content); + response.EnsureSuccessStatusCode(); + + var responseContent = await response.Content.ReadAsStringAsync(); + return InvoiceCreateResponse.FromXml(responseContent); + } + catch (HttpRequestException ex) + { + throw new InnVoiceApiException($"Error creating invoice: {ex.Message}", ex); + } + } + + /// + /// Update an existing invoice + /// + public async Task UpdateInvoiceAsync(int tableId, InvoiceCreateRequest request) + { + // Set the VevoID if updating customer information + var url = $"{_baseUrl}/{_companyName}/invoice"; + + var xml = request.ToXml(); + var content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("data", xml), + new KeyValuePair("id", tableId.ToString()) + }); + + try + { + var response = await _httpClient.PostAsync(url, content); + response.EnsureSuccessStatusCode(); + + var responseContent = await response.Content.ReadAsStringAsync(); + return InvoiceCreateResponse.FromXml(responseContent); + } + catch (HttpRequestException ex) + { + throw new InnVoiceApiException($"Error updating invoice: {ex.Message}", ex); + } + } \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs b/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs index bc12f6b..7e01266 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs @@ -21,6 +21,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions"; private const string OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations"; private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files"; + private const string BaseUrl = "https://api.openai.com/v1"; + + private string? _assistantId; + private string? _vectorStoreId; public OpenAIApiService(ISettingService settingService, HttpClient httpClient) { @@ -286,66 +290,371 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services #endregion #region === PDF ANALYSIS (NEW) === - public async Task AnalyzePdfAsync(string filePath, string userPrompt) - { - // Step 1: Upload PDF - using var form = new MultipartFormDataContent(); - using var fileStream = File.OpenRead(filePath); - var fileContent = new StreamContent(fileStream); - fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); - form.Add(fileContent, "file", Path.GetFileName(filePath)); - form.Add(new StringContent("assistants"), "purpose"); - var uploadResponse = await _httpClient.PostAsync(OpenAiFileEndpoint, form); - if (!uploadResponse.IsSuccessStatusCode) + private async Task EnsureAssistantAndVectorStoreAsync() + { + // Find or create vector store + if (_vectorStoreId == null) { - var error = await uploadResponse.Content.ReadAsStringAsync(); - throw new Exception($"File upload failed: {error}"); + _vectorStoreId = await FindOrCreateVectorStoreAsync("pdf-analysis-store"); } - using var uploadJson = await JsonDocument.ParseAsync(await uploadResponse.Content.ReadAsStreamAsync()); - var fileId = uploadJson.RootElement.GetProperty("id").GetString(); - - // Step 2: Ask model with file reference - var requestBody = new + // Find or create assistant + if (_assistantId == null) { - model = "gpt-4.1", // must support file_search - messages = new[] + _assistantId = await FindOrCreateAssistantAsync("PDF and Image Analyzer Assistant"); + } + } + + //TEMPORARY: Cleanup all assistants (for testing purposes) - A. + public async Task CleanupAllAssistantsAsync() + { + Console.WriteLine("Cleaning up all existing assistants..."); + + var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants"); + listRequest.Headers.Add("OpenAI-Beta", "assistants=v2"); + var response = await _httpClient.SendAsync(listRequest); + + if (response.IsSuccessStatusCode) + { + using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + var assistants = json.RootElement.GetProperty("data"); + + foreach (var assistant in assistants.EnumerateArray()) { - new { role = "system", content = "You are an assistant that analyzes uploaded PDF files." }, - new { role = "user", content = userPrompt } - }, - tools = new[] + var id = assistant.GetProperty("id").GetString(); + var name = assistant.GetProperty("name").GetString(); + + var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"{BaseUrl}/assistants/{id}"); + deleteRequest.Headers.Add("OpenAI-Beta", "assistants=v2"); + await _httpClient.SendAsync(deleteRequest); + + Console.WriteLine($"Deleted assistant: {name} ({id})"); + } + + Console.WriteLine("Cleanup complete!"); + } + + // Reset local cache + _assistantId = null; + } + + public async Task AnalyzePdfAsync(Stream file, string fileName, string userPrompt) + { + + await EnsureAssistantAndVectorStoreAsync(); + var fileId = await UploadFileAsync(file, fileName); + var isImage = IsImageFile(fileName); + + if (!isImage) + { + await AttachFileToVectorStoreAsync(fileId); + } + + var threadId = await CreateThreadAsync(); + + if (isImage) + { + await AddUserMessageWithImageAsync(threadId, userPrompt, fileId); + } + else + { + await AddUserMessageAsync(threadId, userPrompt); + } + + var runId = await CreateRunAsync(threadId); + await WaitForRunCompletionAsync(threadId, runId); + + return await GetAssistantResponseAsync(threadId); + } + + private bool IsImageFile(string fileName) + { + var extension = Path.GetExtension(fileName).ToLowerInvariant(); + return extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".gif" || extension == ".webp"; + } + + private async Task UploadFileAsync(Stream file, string fileName) + { + using var form = new MultipartFormDataContent(); + var fileContent = new StreamContent(file); + + // Determine MIME type based on file extension + var extension = Path.GetExtension(fileName).ToLowerInvariant(); + fileContent.Headers.ContentType = extension switch + { + ".pdf" => new MediaTypeHeaderValue("application/pdf"), + ".jpg" or ".jpeg" => new MediaTypeHeaderValue("image/jpeg"), + ".png" => new MediaTypeHeaderValue("image/png"), + ".gif" => new MediaTypeHeaderValue("image/gif"), + ".webp" => new MediaTypeHeaderValue("image/webp"), + _ => new MediaTypeHeaderValue("application/octet-stream") + }; + + form.Add(fileContent, "file", fileName); + form.Add(new StringContent("assistants"), "purpose"); + + var response = await _httpClient.PostAsync($"{BaseUrl}/files", form); + await EnsureSuccessAsync(response, "upload file"); + + using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + return json.RootElement.GetProperty("id").GetString()!; + } + + private async Task AttachFileToVectorStoreAsync(string fileId) + { + var body = new { file_id = fileId }; + var request = CreateAssistantRequest( + HttpMethod.Post, + $"{BaseUrl}/vector_stores/{_vectorStoreId}/files", + body + ); + + var response = await _httpClient.SendAsync(request); + await EnsureSuccessAsync(response, "attach file to vector store"); + } + + private async Task CreateThreadAsync() + { + var request = CreateAssistantRequest( + HttpMethod.Post, + $"{BaseUrl}/threads", + new { } + ); + + var response = await _httpClient.SendAsync(request); + await EnsureSuccessAsync(response, "create thread"); + + using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + return json.RootElement.GetProperty("id").GetString()!; + } + + private async Task AddUserMessageAsync(string threadId, string userPrompt) + { + var body = new + { + role = "user", + content = userPrompt + }; + + var request = CreateAssistantRequest( + HttpMethod.Post, + $"{BaseUrl}/threads/{threadId}/messages", + body + ); + + var response = await _httpClient.SendAsync(request); + await EnsureSuccessAsync(response, "add user message"); + } + + private async Task AddUserMessageWithImageAsync(string threadId, string userPrompt, string fileId) + { + var body = new + { + role = "user", + content = new object[] { - new { type = "file_search" } - }, + new { type = "text", text = userPrompt }, + new { type = "image_file", image_file = new { file_id = fileId } } + } + }; + + var request = CreateAssistantRequest( + HttpMethod.Post, + $"{BaseUrl}/threads/{threadId}/messages", + body + ); + + var response = await _httpClient.SendAsync(request); + await EnsureSuccessAsync(response, "add user message with image"); + } + + private async Task CreateRunAsync(string threadId) + { + var body = new + { + assistant_id = _assistantId, tool_resources = new { file_search = new { - vector_store_ids = new string[] { fileId! } + vector_store_ids = new[] { _vectorStoreId } } } }; - var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); + var request = CreateAssistantRequest( + HttpMethod.Post, + $"{BaseUrl}/threads/{threadId}/runs", + body + ); - var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); - var chatResponse = await _httpClient.PostAsync(OpenAiEndpoint, requestContent); - chatResponse.EnsureSuccessStatusCode(); + var response = await _httpClient.SendAsync(request); + await EnsureSuccessAsync(response, "create run"); - using var responseJson = await JsonDocument.ParseAsync(await chatResponse.Content.ReadAsStreamAsync()); - var result = responseJson.RootElement - .GetProperty("choices")[0] - .GetProperty("message") - .GetProperty("content") - .GetString(); - - return result ?? "No response from model"; + using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + return json.RootElement.GetProperty("id").GetString()!; } - #endregion + + private async Task WaitForRunCompletionAsync(string threadId, string runId) + { + const int pollIntervalMs = 1000; + const int maxAttempts = 60; // 1 minute timeout + int attempts = 0; + + while (attempts < maxAttempts) + { + var request = CreateAssistantRequest( + HttpMethod.Get, + $"{BaseUrl}/threads/{threadId}/runs/{runId}" + ); + + var response = await _httpClient.SendAsync(request); + await EnsureSuccessAsync(response, "check run status"); + + using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + var status = json.RootElement.GetProperty("status").GetString()!; + + if (status == "completed") + return; + + if (status != "in_progress" && status != "queued") + throw new Exception($"Run failed with status: {status}"); + + await Task.Delay(pollIntervalMs); + attempts++; + } + + throw new TimeoutException("Run did not complete within the expected time"); + } + + private async Task GetAssistantResponseAsync(string threadId) + { + var request = CreateAssistantRequest( + HttpMethod.Get, + $"{BaseUrl}/threads/{threadId}/messages" + ); + + var response = await _httpClient.SendAsync(request); + await EnsureSuccessAsync(response, "retrieve messages"); + + using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + var messages = json.RootElement.GetProperty("data"); + + if (messages.GetArrayLength() == 0) + return "No response"; + + var firstMessage = messages[0] + .GetProperty("content")[0] + .GetProperty("text") + .GetProperty("value") + .GetString(); + + return firstMessage ?? "No response"; + } + + private HttpRequestMessage CreateAssistantRequest(HttpMethod method, string url, object? body = null) + { + var request = new HttpRequestMessage(method, url); + request.Headers.Add("OpenAI-Beta", "assistants=v2"); + + if (body != null) + { + var json = JsonSerializer.Serialize(body, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + } + + return request; + } + + private async Task EnsureSuccessAsync(HttpResponseMessage response, string operation) + { + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"Error Status: {response.StatusCode}"); + Console.WriteLine($"Error Body: {errorBody}"); + throw new Exception($"Failed to {operation}: {errorBody}"); + } + } + + + + private async Task FindOrCreateVectorStoreAsync(string name) + { + // List existing vector stores + var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/vector_stores"); + listRequest.Headers.Add("OpenAI-Beta", "assistants=v2"); + var response = await _httpClient.SendAsync(listRequest); + + if (response.IsSuccessStatusCode) + { + using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + var stores = json.RootElement.GetProperty("data"); + + foreach (var store in stores.EnumerateArray()) + { + if (store.GetProperty("name").GetString() == name) + { + return store.GetProperty("id").GetString()!; + } + } + } + + // Create new if not found + var createBody = new { name = name }; + var createRequest = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/vector_stores", createBody); + var createResponse = await _httpClient.SendAsync(createRequest); + await EnsureSuccessAsync(createResponse, "create vector store"); + + using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync()); + return createJson.RootElement.GetProperty("id").GetString()!; + } + + private async Task FindOrCreateAssistantAsync(string name) + { + // List existing assistants + var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants"); + listRequest.Headers.Add("OpenAI-Beta", "assistants=v2"); + var response = await _httpClient.SendAsync(listRequest); + + if (response.IsSuccessStatusCode) + { + using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + var assistants = json.RootElement.GetProperty("data"); + + foreach (var assistant in assistants.EnumerateArray()) + { + if (assistant.GetProperty("name").GetString() == name) + { + return assistant.GetProperty("id").GetString()!; + } + } + } + + // Create new if not found + var assistantBody = new + { + name = name, + instructions = "You are an assistant that analyzes uploaded files. When you receive an image, analyze and describe what you see in the image in detail. When you receive a PDF or text document, use the file_search tool to find and analyze relevant information. Always respond directly to the user's question about the file they uploaded.", + model = "gpt-4o", + tools = new[] { new { type = "file_search" } } + }; + + var request = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/assistants", assistantBody); + var createResponse = await _httpClient.SendAsync(request); + await EnsureSuccessAsync(createResponse, "create assistant"); + + using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync()); + return createJson.RootElement.GetProperty("id").GetString()!; + } + + #endregion + } + } + diff --git a/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml new file mode 100644 index 0000000..96e8e4e --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml @@ -0,0 +1,61 @@ +@model Nop.Plugin.Misc.FruitBankPlugin.Models.OrderAttributesModel + +
+
+ + Custom Order Attributes +
+
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+ + + 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