merge
This commit is contained in:
commit
4b7b558371
|
|
@ -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<IViewComponentResult> InvokeAsync(TestGridModel model)
|
||||
{
|
||||
// Here you can fetch data for this grid if needed
|
||||
// For demo, just pass the model
|
||||
return View(model.ViewComponentLocation, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
<h4>Id: @Model.ShippingId</h4>
|
||||
|
||||
@(Html.DevExtreme().DataGrid<ShippingDocument>()
|
||||
.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);
|
||||
})
|
||||
)
|
||||
|
|
@ -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<IActionResult> 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 });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string> GetAttributesXmlForProductAttributeCombinationAsync(IFormCollection form, List<string> 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<int> 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<string>();
|
||||
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<List<BulkEditData>> ParseBulkEditDataAsync()
|
||||
{
|
||||
var rez = new Dictionary<int, BulkEditData>();
|
||||
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<string, StringValues> 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<BulkEditData> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> ProductList(ProductSearchModel searchModel)
|
||||
{
|
||||
//prepare model
|
||||
var model = await _productModelFactory.PrepareProductListModelAsync(searchModel);
|
||||
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
|
||||
public virtual async Task<IActionResult> BulkEditProducts(ProductSearchModel searchModel)
|
||||
{
|
||||
//prepare model
|
||||
var model = await _productModelFactory.PrepareProductListModelAsync(searchModel);
|
||||
var html = await RenderPartialViewToStringAsync("_BulkEdit.Products", model.Data.ToList());
|
||||
|
||||
return Json(new Dictionary<string, object> { { "Html", html }, { "Products", model } });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
|
||||
public virtual async Task<IActionResult> BulkEditNewProduct(int id)
|
||||
{
|
||||
var primaryStoreCurrencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId)).CurrencyCode;
|
||||
|
||||
//prepare model
|
||||
var model = new List<ProductModel> { 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<IActionResult> 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<IActionResult> 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<bool>(customer, NopCustomerDefaults.HideConfigurationStepsAttribute);
|
||||
var closeCard = await _genericAttributeService.GetAttributeAsync<bool>(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<IActionResult> 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>();
|
||||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> DeleteSelected(ICollection<int> 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<IActionResult> 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<IActionResult> 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<IActionResult> LoadProductFriendlyNames(string productIds)
|
||||
{
|
||||
var result = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(productIds))
|
||||
return Json(new { Text = result });
|
||||
|
||||
var ids = new List<int>();
|
||||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<ProductSpecificationAttribute>();
|
||||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> ProductTags()
|
||||
{
|
||||
//prepare model
|
||||
var model = await _productModelFactory.PrepareProductTagSearchModelAsync(new ProductTagSearchModel());
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_VIEW)]
|
||||
public virtual async Task<IActionResult> 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<IActionResult> 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<IActionResult> ProductTagsDelete(ICollection<int> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<int> { 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<int> { 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<IActionResult> 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<int> { 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<int> { 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<IActionResult> ExportXmlSelected(string selectedIds)
|
||||
{
|
||||
var products = new List<Product>();
|
||||
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<IActionResult> 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<int> { 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<int> { 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<IActionResult> ExportExcelSelected(string selectedIds)
|
||||
{
|
||||
var products = new List<Product>();
|
||||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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>();
|
||||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<ProductAttributeMapping>();
|
||||
|
||||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<ProductAttributeValue>();
|
||||
|
||||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<ProductAttributeMapping> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<string>();
|
||||
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<ProductAttributeCombination>();
|
||||
|
||||
//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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<string>();
|
||||
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<IActionResult> 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<IActionResult> SaveProductEditorSettings(ProductModel model, string returnUrl = "")
|
||||
{
|
||||
//vendors cannot manage these settings
|
||||
if (await _workContext.GetCurrentVendorAsync() != null)
|
||||
return RedirectToAction("List");
|
||||
|
||||
var productEditorSettings = await _settingService.LoadSettingAsync<ProductEditorSettings>();
|
||||
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<IActionResult> 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
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DevExtreme.NETCore.Demos.Controllers
|
||||
{
|
||||
public class FileManagerController : Controller
|
||||
{
|
||||
public IActionResult BindingToFileSystem()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IActionResult> 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<TestGridModel>();
|
||||
var childGrid1 = new TestGridModel
|
||||
{
|
||||
GridName = "TestGrid",
|
||||
ViewComponentName = "TestGridComponent",
|
||||
ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml",
|
||||
ParentGridId = testGridModel2.Id,
|
||||
ChildGrids = new List<TestGridModel>()
|
||||
};
|
||||
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<TestGridModel>()
|
||||
};
|
||||
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<string, int>();
|
||||
|
||||
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<IActionResult> 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<IActionResult> UploadFile(List<IFormFile> 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<Files>();
|
||||
var shippingDocumentToFileList = new List<ShippingDocumentToFiles>();
|
||||
|
||||
|
||||
|
||||
//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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
using Nop.Web.Framework.Models;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.Catalog;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a product list model
|
||||
/// </summary>
|
||||
public partial record ProductListModel : BasePagedListModel<ProductModel>
|
||||
{
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents a grid with potential child grids
|
||||
/// </summary>
|
||||
public class TestGridModel
|
||||
{
|
||||
public Guid Id = Guid.NewGuid();
|
||||
public TestGridModel()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
ChildGrids = new List<TestGridModel>();
|
||||
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<TestGridModel> ChildGrids { get; set; }
|
||||
|
||||
// Grid Behavior Configuration
|
||||
public GridConfiguration Configuration { get; set; }
|
||||
|
||||
// Data Context (optional - for passing entity IDs or filter params)
|
||||
public Dictionary<string, int> DataContext { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for grid rendering and behavior
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder class for easier model construction
|
||||
/// </summary>
|
||||
public class TestPageModelBuilder
|
||||
{
|
||||
private readonly TestPageModel _model;
|
||||
private readonly Dictionary<Guid, TestGridModel> _gridLookup;
|
||||
|
||||
public TestPageModelBuilder()
|
||||
{
|
||||
_model = new TestPageModel();
|
||||
_gridLookup = new Dictionary<Guid, TestGridModel>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example usage helper
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
|
|||
{
|
||||
public class TestPageModel
|
||||
{
|
||||
public TestPageModel()
|
||||
{
|
||||
Grids = new List<TestGridModel>();
|
||||
}
|
||||
|
||||
public List<TestGridModel> Grids { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
|
||||
<form method="post" enctype="multipart/form-data" asp-controller="ManagementPage" asp-action="UploadFile">
|
||||
@(Html.DevExtreme().FileUploader()
|
||||
.ID("shippingDocumentUploader-" + contextId)
|
||||
.Name("files")
|
||||
.Multiple(true)
|
||||
.Accept("application/pdf")
|
||||
.UploadMode(FileUploadMode.UseForm)
|
||||
)
|
||||
<input type="hidden" name="ShippingDocumentId" value="@contextId" />
|
||||
|
||||
<input type="hidden" name="PartnerId" value="hello" />
|
||||
|
||||
@(Html.DevExtreme().Button()
|
||||
.Text("Upload Files")
|
||||
.Type(ButtonType.Success)
|
||||
.UseSubmitBehavior(true)
|
||||
)
|
||||
</form>
|
||||
<div class="content" id="selected-files">
|
||||
<div>
|
||||
<h4>Selected Files</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@(Html.DevExtreme().FileManager()
|
||||
.ID(fileManagerId)
|
||||
.FileSystemProvider(provider => provider.Remote()
|
||||
.Url(Url.RouteUrl("FileManagementFileSystemApi"))
|
||||
.BeforeAjaxSend(@<text>
|
||||
function(arg) {
|
||||
arg.headers.TestHeader = @Model.DataContext["contextId"];
|
||||
}
|
||||
</text>))
|
||||
.Permissions(permissions => {
|
||||
permissions.Download(true);
|
||||
permissions.Upload(true);
|
||||
})
|
||||
.AllowedFileExtensions(new[] { ".pdf", ".jpg", ".jpeg" })
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
function fileUploader_valueChanged(e) {
|
||||
var files = e.value;
|
||||
if(files.length > 0) {
|
||||
$("#selected-files .selected-item").remove();
|
||||
|
||||
$.each(files, function(i, file) {
|
||||
var $selectedItem = $("<div />").addClass("selected-item");
|
||||
$selectedItem.append(
|
||||
$("<span />").html("Name: " + file.name + "<br/>"),
|
||||
$("<span />").html("Size " + file.size + " bytes" + "<br/>"),
|
||||
$("<span />").html("Type " + file.type + "<br/>"),
|
||||
$("<span />").html("Last Modified Date: " + file.lastModifiedDate)
|
||||
);
|
||||
$selectedItem.appendTo($("#selected-files"));
|
||||
});
|
||||
$("#selected-files").show();
|
||||
}
|
||||
else
|
||||
$("#selected-files").hide();
|
||||
}
|
||||
|
||||
function getGridInstance() {
|
||||
return $("#shippingDocumentUploader").dxFileUploader("instance");
|
||||
}
|
||||
|
||||
function fileUploader_fileUploaded(e) {
|
||||
const fileUploaderId = e.component.element().attr('id');
|
||||
|
||||
// 2. Extract the number from the ID
|
||||
const match = fileUploaderId.match(/\d+$/);
|
||||
|
||||
if (match) {
|
||||
const uniqueId = match[0];
|
||||
const gridId = `shippingDocumentGridContainer-${uniqueId}`;
|
||||
|
||||
// 3. Get the DevExtreme grid instance and refresh it
|
||||
const grid = $(`#${gridId}`).dxDataGrid('instance');
|
||||
|
||||
if (grid) {
|
||||
grid.dxDataGrid("getDataSource").reload();
|
||||
// Optional: Show a success notification
|
||||
DevExpress.ui.notify("Documents updated successfully!", "success", 2000);
|
||||
} else {
|
||||
console.error(`DevExtreme grid with ID "${gridId}" not found.`);
|
||||
}
|
||||
} else {
|
||||
console.error("Could not find a unique ID number from the file uploader.");
|
||||
}
|
||||
// shippingDocumentGridContainer
|
||||
//$("#shippingDocumentGridContainer" + e.component.ID).dxDataGrid("getDataSource").reload();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
@ -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 @@
|
|||
</div>
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<h1>RTTTTTTTTTT</h1>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<nop-doc-reference asp-string-resource="@T("Admin.Documentation.Reference.Orders", Docs.Orders + Utm.OnAdmin)" />
|
||||
|
||||
|
|
@ -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)} <br /><a href="${link}">${data}</a > `;
|
||||
}
|
||||
|
||||
function renderColumnNeedsMeasurement(data, type, row, meta) {
|
||||
function renderColumnIsMeasurable(data, type, row, meta) {
|
||||
if(data === true) {
|
||||
return '<span class="badge badge-warning">Yes</span>';
|
||||
return '<span class="badge badge-warning" disabled>Yes</span>';
|
||||
}
|
||||
return '<span class="badge badge-secondary">No</span>';
|
||||
return '<span class="badge badge-secondary" disabled>No</span>';
|
||||
}
|
||||
|
||||
function renderColumnPickupDateAndTime(data, type, row, meta) {
|
||||
|
||||
return `<span>${data}</span>`;
|
||||
}
|
||||
|
||||
$(function() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
<div>
|
||||
@(
|
||||
|
|
@ -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(@<text>
|
||||
<div class="master-detail-caption"><%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:</div>
|
||||
<div id="fileuploader">
|
||||
<div class="widget-container">
|
||||
|
||||
<section id="tabs" class="project-tab">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<nav>
|
||||
<div class="nav nav-tabs nav-fill" id="nav-tab" role="tablist">
|
||||
<a class="nav-item nav-link active" id="nav-home-tab" data-toggle="tab" href="#nav-home" role="tab" aria-controls="nav-home" aria-selected="true">Partner info</a>
|
||||
<a class="nav-item nav-link" id="nav-profile-tab" data-toggle="tab" href="#nav-profile" role="tab" aria-controls="nav-profile" aria-selected="false">Products</a>
|
||||
<a class="nav-item nav-link" id="nav-contact-tab" data-toggle="tab" href="#nav-contact" role="tab" aria-controls="nav-contact" aria-selected="false">Files</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
<div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab">
|
||||
<table class="table" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project Name</th>
|
||||
<th>Employer</th>
|
||||
<th>Awards</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="#">Work 1</a></td>
|
||||
<td>Doe</td>
|
||||
<td>john@example.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="#">Work 2</a></td>
|
||||
<td>Moe</td>
|
||||
<td>mary@example.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="#">Work 3</a></td>
|
||||
<td>Dooley</td>
|
||||
<td>july@example.com</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab">
|
||||
<table class="table" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project Name</th>
|
||||
<th>Employer</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="#">Work 1</a></td>
|
||||
<td>Doe</td>
|
||||
<td>john@example.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="#">Work 2</a></td>
|
||||
<td>Moe</td>
|
||||
<td>mary@example.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="#">Work 3</a></td>
|
||||
<td>Dooley</td>
|
||||
<td>july@example.com</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-contact" role="tabpanel" aria-labelledby="nav-contact-tab">
|
||||
<form method="post" enctype="multipart/form-data" asp-controller="ManagementPage" asp-action="UploadFile">
|
||||
@(Html.DevExtreme().FileUploader()
|
||||
.ID(new JS("'shippingDocumentUploader-' + data.Id"))
|
||||
.Name("files")
|
||||
.Multiple(true)
|
||||
.Accept("application/pdf")
|
||||
.UploadMode(FileUploadMode.UseForm)
|
||||
.MasterDetail(md => md.Enabled(true).Template(new TemplateName("masterDetailTemplate")))
|
||||
)
|
||||
|
||||
<input type="hidden" name="ShippingDocumentId" value="<%- data.Id %>" />
|
||||
<% if (data.PartnerId) { %>
|
||||
<input type="hidden" name="PartnerId" value="<%- data.PartnerId %>" />
|
||||
<% } %>
|
||||
|
||||
@(Html.DevExtreme().Button()
|
||||
.Text("Upload Files")
|
||||
.Type(ButtonType.Success)
|
||||
.UseSubmitBehavior(true)
|
||||
)
|
||||
</form>
|
||||
|
||||
<div class="content" id="selected-files">
|
||||
<div>
|
||||
<h4>Selected Files</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* <div id="@(new JS("'shippingDocumentGridContainer-' + data.ID"))"></div>
|
||||
@(Html.DevExtreme().DataGrid<FruitBank.Common.Entities.ShippingDocument>()
|
||||
.ColumnAutoWidth(true)
|
||||
.ShowBorders(true)
|
||||
.ID(new JS("'shippingDocumentGridContainer-' + data.Id"))
|
||||
.Columns(columns => {
|
||||
columns.AddFor(m => m.Id).AllowEditing(false);
|
||||
|
||||
columns.AddFor(m => m.Country);
|
||||
|
||||
columns.AddFor(m => m.Created);
|
||||
|
||||
columns.AddFor(m => m.PartnerId);
|
||||
|
||||
columns.Add()
|
||||
.Caption("Completed")
|
||||
.DataType(GridColumnDataType.Boolean)
|
||||
.CalculateCellValue("calculateCellValue");
|
||||
})
|
||||
.DataSource(ds => ds.Mvc()
|
||||
.Controller("Shipping")
|
||||
.LoadAction("GetShippingDocumentsByShippingId")
|
||||
.LoadParams(new { shippingId = new JS("data.Id") })
|
||||
.Key("Id")
|
||||
)
|
||||
) *@
|
||||
@using (Html.DevExtreme().NamedTemplate("masterDetailTemplate"))
|
||||
{
|
||||
<div class="master-detail-caption">
|
||||
<%- data.ShippingDate %> <%- data.LicencePlate %>'s shipping documents:
|
||||
</div>
|
||||
|
||||
@if (Model.ChildGrids != null && Model.ChildGrids.Any())
|
||||
{
|
||||
<div class="@Model.Configuration.ChildGridContainerCssClass mt-3" data-parent-id="<%- data.Id %>">
|
||||
@if (Model.Configuration.ShowChildGridsAsTabs)
|
||||
{
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
@for (int i = 0; i < Model.ChildGrids.Count; i++)
|
||||
{
|
||||
var child = Model.ChildGrids[i];
|
||||
var isActive = i == 0 ? "active" : "";
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @isActive"
|
||||
data-toggle="tab"
|
||||
data-child-index="@i"
|
||||
href="#content-@child.Id-<%- data.Id %>"
|
||||
onclick="reloadChildGrid(this, '<%- data.Id %>', @i)">
|
||||
@child.GridName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mt-2">
|
||||
@for (int i = 0; i < Model.ChildGrids.Count; i++)
|
||||
{
|
||||
var child = Model.ChildGrids[i];
|
||||
var isActive = i == 0 ? "show active" : "";
|
||||
|
||||
<div class="tab-pane fade @isActive"
|
||||
id="content-@child.Id-<%- data.Id %>"
|
||||
data-child-index="@i">
|
||||
@* Initial load - will be replaced by AJAX on first tab click *@
|
||||
<div class="text-center p-3">
|
||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</text>);
|
||||
})
|
||||
)
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
<script>
|
||||
// Store the parent grid model as JSON
|
||||
var parentGridModel = @Html.Raw(Json.Serialize(Model));
|
||||
|
||||
// Global function to reload child grids
|
||||
function reloadChildGrid(tabElement, contextId, childIndex) {
|
||||
const $tab = $(tabElement);
|
||||
const $contentPane = $($tab.attr('href'));
|
||||
|
||||
// Check if already loaded
|
||||
if ($contentPane.data('loaded')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
$contentPane.html('<div class="text-center p-3"><span class="spinner-border spinner-border-sm"></span> Loading...</div>');
|
||||
|
||||
// Get the child model from the parent model
|
||||
var childModel = parentGridModel.ChildGrids[childIndex];
|
||||
|
||||
$.ajax({
|
||||
url: '@Url.Action("LoadChildGrid", "ManagementPage")',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val()
|
||||
},
|
||||
data: JSON.stringify({
|
||||
contextId: contextId,
|
||||
childModel: childModel
|
||||
}),
|
||||
success: function (data) {
|
||||
$contentPane.html(data);
|
||||
$contentPane.data('loaded', true);
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
$contentPane.html('<div class="alert alert-danger">Error loading grid: ' + error + '</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load first tab automatically when master detail opens
|
||||
$(document).ready(function() {
|
||||
$('#orderDataGridContainer').on('contentReady', function() {
|
||||
// This will trigger when master detail rows are expanded
|
||||
$('.nav-tabs .nav-link.active').each(function() {
|
||||
const contextId = $(this).closest('[data-parent-id]').attr('data-parent-id');
|
||||
const childIndex = $(this).data('child-index');
|
||||
if (contextId && childIndex !== undefined) {
|
||||
reloadChildGrid(this, contextId, childIndex);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function calculateCellValue(rowData) {
|
||||
return rowData.Status === "Completed";
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
|
||||
function calculateItemsCount(rowData) {
|
||||
return rowData.ShippingItems.length;
|
||||
return rowData.ShippingItems ? rowData.ShippingItems.length : 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
function onDeleteBtnClick(){
|
||||
let dataGrid = $("#gridContainer").dxDataGrid("instance");
|
||||
function onDeleteBtnClick() {
|
||||
let dataGrid = $("#orderDataGridContainer").dxDataGrid("instance");
|
||||
$.when.apply($, dataGrid.getSelectedRowsData().map(function(data) {
|
||||
return dataGrid.getDataSource().store().remove(data.ID);
|
||||
return dataGrid.getDataSource().store().remove(data.Id);
|
||||
})).done(function() {
|
||||
dataGrid.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
function calculateFilterExpression(filterValue, selectedFilterOperation, target) {
|
||||
if(target === "search" && typeof(filterValue) === "string") {
|
||||
return [this.dataField, "contains", filterValue]
|
||||
}
|
||||
return function(data) {
|
||||
return (data.AssignedEmployee || []).indexOf(filterValue) !== -1
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChanged(data) {
|
||||
let dataGrid = $("#gridContainer").dxDataGrid("instance");
|
||||
let dataGrid = $("#orderDataGridContainer").dxDataGrid("instance");
|
||||
dataGrid.option("toolbar.items[1].options.disabled", !data.selectedRowsData.length);
|
||||
}
|
||||
|
||||
function onRowExpanded(e) {
|
||||
e.component.dxDataGrid("getDataSource").reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
||||
function fileUploader_valueChanged(e) {
|
||||
var files = e.value;
|
||||
if(files.length > 0) {
|
||||
$("#selected-files .selected-item").remove();
|
||||
|
||||
$.each(files, function(i, file) {
|
||||
var $selectedItem = $("<div />").addClass("selected-item");
|
||||
$selectedItem.append(
|
||||
$("<span />").html("Name: " + file.name + "<br/>"),
|
||||
$("<span />").html("Size " + file.size + " bytes" + "<br/>"),
|
||||
$("<span />").html("Type " + file.type + "<br/>"),
|
||||
$("<span />").html("Last Modified Date: " + file.lastModifiedDate)
|
||||
);
|
||||
$selectedItem.appendTo($("#selected-files"));
|
||||
});
|
||||
$("#selected-files").show();
|
||||
// Trigger loading of first tab when row expands
|
||||
const $firstTab = $(e.element).find('.master-detail-caption').next().find('.nav-link.active').first();
|
||||
if ($firstTab.length) {
|
||||
const contextId = e.key; // This is the actual row's Id (data.Id)
|
||||
const childIndex = $firstTab.data('child-index');
|
||||
if (contextId && childIndex !== undefined) {
|
||||
setTimeout(() => reloadChildGrid($firstTab[0], contextId, childIndex), 100);
|
||||
}
|
||||
}
|
||||
else
|
||||
$("#selected-files").hide();
|
||||
}
|
||||
|
||||
function getGridInstance() {
|
||||
return $("#shippingDocumentUploader").dxFileUploader("instance");
|
||||
}
|
||||
|
||||
function fileUploader_fileUploaded(e) {
|
||||
const fileUploaderId = e.component.element().attr('id');
|
||||
|
||||
// 2. Extract the number from the ID
|
||||
const match = fileUploaderId.match(/\d+$/);
|
||||
|
||||
if (match) {
|
||||
const uniqueId = match[0];
|
||||
const gridId = `shippingDocumentGridContainer-${uniqueId}`;
|
||||
|
||||
// 3. Get the DevExtreme grid instance and refresh it
|
||||
const grid = $(`#${gridId}`).dxDataGrid('instance');
|
||||
|
||||
if (grid) {
|
||||
grid.dxDataGrid("getDataSource").reload();
|
||||
// Optional: Show a success notification
|
||||
DevExpress.ui.notify("Documents updated successfully!", "success", 2000);
|
||||
} else {
|
||||
console.error(`DevExtreme grid with ID "${gridId}" not found.`);
|
||||
}
|
||||
} else {
|
||||
console.error("Could not find a unique ID number from the file uploader.");
|
||||
}
|
||||
// shippingDocumentGridContainer
|
||||
//$("#shippingDocumentGridContainer" + e.component.ID).dxDataGrid("getDataSource").reload();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
|
||||
@using System.Text.Json
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h4>Test Child Grid View Component</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Grid Information:</h5>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<td><strong>Grid ID:</strong></td>
|
||||
<td>@Model.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Grid Name:</strong></td>
|
||||
<td>@Model.GridName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>View Component Name:</strong></td>
|
||||
<td>@Model.ViewComponentName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Level:</strong></td>
|
||||
<td>@Model.Level</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Parent Grid ID:</strong></td>
|
||||
<td>@(Model.ParentGridId?.ToString() ?? "None")</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@if (Model.DataContext != null && Model.DataContext.Any())
|
||||
{
|
||||
<h5 class="mt-3">Data Context:</h5>
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var kvp in Model.DataContext)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@kvp.Key</strong></td>
|
||||
<td>@kvp.Value</td>
|
||||
<td><em>@kvp.Value.GetType().Name</em></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if (Model.DataContext.ContainsKey("contextId"))
|
||||
{
|
||||
<div class="alert alert-success mt-3">
|
||||
<strong>Context ID Found:</strong> @Model.DataContext["contextId"]
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning mt-3">
|
||||
<strong>Warning:</strong> No contextId found in DataContext
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning mt-3">
|
||||
<strong>No Data Context available</strong>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.Configuration != null)
|
||||
{
|
||||
<h5 class="mt-3">Configuration:</h5>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<td><strong>Show Child Grids As Tabs:</strong></td>
|
||||
<td>@Model.Configuration.ShowChildGridsAsTabs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Requires Parent Row Selection:</strong></td>
|
||||
<td>@Model.Configuration.RequiresParentRowSelection</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Description:</strong></td>
|
||||
<td>@(Model.Configuration.Description ?? "N/A")</td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (Model.ChildGrids != null && Model.ChildGrids.Any())
|
||||
{
|
||||
<div class="alert alert-info mt-3">
|
||||
<strong>Child Grids:</strong> This grid has @Model.ChildGrids.Count child grid(s)
|
||||
<ul class="mb-0 mt-2">
|
||||
@foreach (var child in Model.ChildGrids)
|
||||
{
|
||||
<li>@child.GridName (Level @child.Level)</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-secondary mt-3">
|
||||
<strong>No child grids</strong>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h5 class="mt-3">Full Model JSON:</h5>
|
||||
<pre class="bg-light p-3" style="max-height: 300px; overflow-y: auto;">@JsonSerializer.Serialize(Model, new JsonSerializerOptions { WriteIndented = true })</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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<bool>(await workContext.GetCurrentCustomerAsync(), hideSearchBlockAttributeName);
|
||||
}
|
||||
|
||||
@if (Model.LicenseCheckModel.BlockPages != true)
|
||||
{
|
||||
<form asp-controller="Product" asp-action="List" method="post">
|
||||
<div class="content-header clearfix">
|
||||
<h1 class="float-left">
|
||||
Fruitbank @T("Admin.Catalog.Products")
|
||||
</h1>
|
||||
<div class="float-right">
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="fas fa-square-plus"></i>
|
||||
@T("Admin.Common.AddNew")
|
||||
</a>
|
||||
<a asp-action="BulkEdit" class="btn btn-info">
|
||||
<i class="fas fa-pen"></i>
|
||||
@T("Admin.Catalog.Products.BulkEdit")
|
||||
</a>
|
||||
<button asp-action="DownloadCatalogPDF" type="submit" name="download-catalog-pdf" class="btn bg-purple">
|
||||
<i class="far fa-file-pdf"></i>
|
||||
@T("Admin.Catalog.Products.List.DownloadPDF")
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-success">
|
||||
<i class="fas fa-download"></i>
|
||||
@T("Admin.Common.Export")
|
||||
</button>
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
<span class="sr-only"> </span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li class="dropdown-item">
|
||||
|
||||
<button asp-action="ExportToXml" type="submit" name="exportxml-all">
|
||||
<i class="far fa-file-code"></i>
|
||||
@T("Admin.Common.ExportToXml.All")
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
|
||||
<button type="button" id="exportxml-selected">
|
||||
<i class="far fa-file-code"></i>
|
||||
@T("Admin.Common.ExportToXml.Selected")
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li class="dropdown-item">
|
||||
<button asp-action="ExportToExcel" type="submit" name="exportexcel-all">
|
||||
<i class="far fa-file-excel"></i>
|
||||
@T("Admin.Common.ExportToExcel.All")
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<button type="button" id="exportexcel-selected">
|
||||
<i class="far fa-file-excel"></i>
|
||||
@T("Admin.Common.ExportToExcel.Selected")
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@if (!Model.IsLoggedInAsVendor || Model.AllowVendorsToImportProducts)
|
||||
{
|
||||
//a vendor cannot import products
|
||||
<button type="button" name="importexcel" class="btn bg-olive" data-toggle="modal" data-target="#importexcel-window">
|
||||
<i class="fas fa-upload"></i>
|
||||
@T("Admin.Common.Import")
|
||||
</button>
|
||||
}
|
||||
<button type="button" id="delete-selected" class="btn btn-danger">
|
||||
<i class="far fa-trash-can"></i>
|
||||
@T("Admin.Common.Delete.Selected")
|
||||
</button>
|
||||
<nop-action-confirmation asp-button-id="delete-selected" />
|
||||
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.ProductListButtons, additionalData = Model })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="form-horizontal">
|
||||
<div class="cards-group">
|
||||
<div class="card card-default card-search">
|
||||
<div class="card-body">
|
||||
<div class="row search-row @(!hideSearchBlock ? "opened" : "")" data-hideAttribute="@hideSearchBlockAttributeName">
|
||||
<div class="search-text">@T("Admin.Common.Search")</div>
|
||||
<div class="icon-search"><i class="fas fa-magnifying-glass" aria-hidden="true"></i></div>
|
||||
<div class="icon-collapse"><i class="far fa-angle-@(!hideSearchBlock ? "up" : "down")" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
|
||||
<div class="search-body @(hideSearchBlock ? "closed" : "")">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchProductName" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-editor asp-for="SearchProductName" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" @(Model.AvailableCategories.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchCategoryId" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-select asp-for="SearchCategoryId" asp-items="Model.AvailableCategories" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" @(Model.AvailableCategories.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchIncludeSubCategories" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-editor asp-for="SearchIncludeSubCategories" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" @(Model.AvailableManufacturers.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchManufacturerId" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-select asp-for="SearchManufacturerId" asp-items="Model.AvailableManufacturers" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" @(Model.AvailableVendors.SelectionIsNotPossible() || Model.IsLoggedInAsVendor ? Html.Raw("style='display: none;'") : null)>
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchVendorId" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-select asp-for="SearchVendorId" asp-items="Model.AvailableVendors" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="form-group row" @(Model.HideStoresList ? Html.Raw("style=\"display:none\"") : null)>
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchStoreId" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-select asp-for="SearchStoreId" asp-items="Model.AvailableStores" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" @(Model.AvailableWarehouses.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchWarehouseId" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-select asp-for="SearchWarehouseId" asp-items="Model.AvailableWarehouses" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchProductTypeId" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-select asp-for="SearchProductTypeId" asp-items="Model.AvailableProductTypes" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="SearchPublishedId" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<nop-select asp-for="SearchPublishedId" asp-items="Model.AvailablePublishedOptions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-4">
|
||||
<nop-label asp-for="GoDirectlyToSku" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="input-group input-group-short">
|
||||
<nop-editor asp-for="GoDirectlyToSku" />
|
||||
<span class="input-group-append">
|
||||
<button type="submit" id="go-to-product-by-sku" name="go-to-product-by-sku" class="btn btn-info btn-flat">
|
||||
@T("Admin.Common.Go")
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="text-center col-12">
|
||||
<button type="button" id="search-products" class="btn btn-primary btn-search">
|
||||
<i class="fas fa-magnifying-glass"></i>
|
||||
@T("Admin.Common.Search")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-body">
|
||||
<nop-doc-reference asp-string-resource="@T("Admin.Documentation.Reference.Products", Docs.Products + Utm.OnAdmin)" />
|
||||
|
||||
@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<FilterParameter>
|
||||
{
|
||||
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<ColumnProperty>
|
||||
{
|
||||
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"))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
<script>
|
||||
|
||||
$(function() {
|
||||
$('#delete-selected-action-confirmation-submit-button').bind('click', function () {
|
||||
var postData = {
|
||||
selectedIds: selectedIds
|
||||
};
|
||||
addAntiForgeryToken(postData);
|
||||
$.ajax({
|
||||
cache: false,
|
||||
type: "POST",
|
||||
url: "@(Url.Action("DeleteSelected", "Product"))",
|
||||
data: postData,
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showAlert('deleteSelectedFailed', errorThrown);
|
||||
},
|
||||
complete: function (jqXHR, textStatus) {
|
||||
if (jqXHR.status === 204)
|
||||
{
|
||||
showAlert('nothingSelectedAlert', '@T("Admin.Common.Alert.NothingSelected")');
|
||||
return;
|
||||
}
|
||||
updateTable('#products-grid');
|
||||
}
|
||||
});
|
||||
$('#delete-selected-action-confirmation').modal('toggle');
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
<nop-alert asp-alert-id="deleteSelectedFailed" />
|
||||
<nop-alert asp-alert-id="nothingSelectedAlert" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
$(function() {
|
||||
$("#@Html.IdFor(model => model.GoDirectlyToSku)").keydown(function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
$("#go-to-product-by-sku").trigger("click");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
}
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
var displayModal = @((Model.LicenseCheckModel?.DisplayWarning == true || Model.LicenseCheckModel?.BlockPages == true).ToString().ToLower());
|
||||
if (displayModal) {
|
||||
$('#license-window').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
$('#license-window').on('shown.bs.modal', function (event) {
|
||||
var modalCloseEl = $(this).find('button.close');
|
||||
var closeTextEl = $('span', modalCloseEl);
|
||||
|
||||
var startFrom = 5;
|
||||
closeTextEl.text(startFrom);
|
||||
|
||||
const timer = setInterval(function() {
|
||||
if (startFrom-- > 0)
|
||||
closeTextEl.text(startFrom);
|
||||
}, 1000);
|
||||
|
||||
setTimeout(function() {
|
||||
closeTextEl.html('×');
|
||||
modalCloseEl.on('click', function() {
|
||||
$('#license-window').modal('hide')
|
||||
});
|
||||
clearInterval(timer);
|
||||
}, startFrom*1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div id="license-window" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
@Html.Raw(Model.LicenseCheckModel?.WarningText)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@*import products form*@
|
||||
<div id="importexcel-window" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="importexcel-window-title">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="importexcel-window-title">@T("Admin.Common.ImportFromExcel")</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<form asp-controller="Product" asp-action="ImportExcel" method="post" enctype="multipart/form-data">
|
||||
<div class="form-horizontal">
|
||||
<div class="modal-body">
|
||||
<ul class="common-list">
|
||||
<li>
|
||||
<em>@T("Admin.Catalog.Products.List.ImportFromExcelTip")</em>
|
||||
</li>
|
||||
<li>
|
||||
<em>@T("Admin.Common.ImportFromExcel.ManyRecordsWarning")</em>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-2">
|
||||
<div class="label-wrapper">
|
||||
<label class="col-form-label">
|
||||
@T("Admin.Common.ExcelFile")
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<input type="file" id="importexcelfile" name="importexcelfile" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@T("Admin.Common.ImportFromExcel")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@*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*@
|
||||
<form asp-controller="Product" asp-action="ExportXmlSelected" method="post" id="export-xml-selected-form">
|
||||
<input type="hidden" id="selectedIds" name="selectedIds" value="" />
|
||||
</form>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('#exportxml-selected').click(function (e) {
|
||||
e.preventDefault();
|
||||
var ids = selectedIds.join(",");
|
||||
if (!ids) {
|
||||
$('#exportXmlSelected-info').text("@T("Admin.Products.NoProducts")");
|
||||
$("#exportXmlSelected").trigger("click");
|
||||
}
|
||||
else {
|
||||
$('#export-xml-selected-form #selectedIds').val(ids);
|
||||
$('#export-xml-selected-form').submit();
|
||||
updateTable('#products-grid');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<nop-alert asp-alert-id="exportXmlSelected" />
|
||||
|
||||
@*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*@
|
||||
<form asp-controller="Product" asp-action="ExportExcelSelected" method="post" id="export-excel-selected-form">
|
||||
<input type="hidden" id="selectedIds" name="selectedIds" value="" />
|
||||
</form>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('#exportexcel-selected').click(function (e) {
|
||||
e.preventDefault();
|
||||
var ids = selectedIds.join(",");
|
||||
if (!ids) {
|
||||
$('#exportExcelSelected-info').text("@T("Admin.Products.NoProducts")");
|
||||
$("#exportExcelSelected").trigger("click");
|
||||
}
|
||||
else {
|
||||
$('#export-excel-selected-form #selectedIds').val(ids);
|
||||
$('#export-excel-selected-form').submit();
|
||||
updateTable('#products-grid');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<nop-alert asp-alert-id="exportExcelSelected" />
|
||||
|
|
@ -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<IViewComponentResult> 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<Order, DateTime?>(model.OrderId, nameof(IOrderDto.DateOfReceipt));
|
||||
var orderMeasurableAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Order, bool>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OrderSearchModel> PrepareOrderSearchModelAsync(OrderSearchModel searchModel)
|
||||
|
|
@ -173,7 +177,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
|||
}
|
||||
|
||||
public override async Task<OrderListModel> PrepareOrderListModelAsync(OrderSearchModel searchModel)
|
||||
=>await base.PrepareOrderListModelAsync(searchModel);
|
||||
=> await base.PrepareOrderListModelAsync(searchModel);
|
||||
|
||||
public async Task<OrderListModelExtended> 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<DateTime?> GetPickupDateTimeAsync(OrderModel order)
|
||||
{
|
||||
DateTime? dateTime = DateTime.MinValue;
|
||||
var fullOrder = await _orderService.GetOrderByIdAsync(order.Id);
|
||||
if (fullOrder != null)
|
||||
{
|
||||
dateTime = await _genericAttributeService.GetAttributeAsync<DateTime>(fullOrder, nameof(IOrderDto.DateOfReceipt));
|
||||
if(dateTime == DateTime.MinValue || !dateTime.HasValue)
|
||||
{
|
||||
dateTime = null;
|
||||
}
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IList<string>> GetWidgetZonesAsync()
|
||||
{
|
||||
return Task.FromResult<IList<string>>(new List<string> { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock });
|
||||
return Task.FromResult<IList<string>>(new List<string> { 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;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Services\InnvoiceApiService.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="logo.jpg" />
|
||||
|
|
@ -28,7 +31,10 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
|
||||
<PackageReference Include="PdfPig" Version="0.1.11" />
|
||||
<PackageReference Include="PdfPig.Rendering.Skia" Version="0.1.11.5" />
|
||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||
<PackageReference Include="Tesseract" Version="5.2.0" />
|
||||
<PackageReference Include="TesseractOCR" Version="5.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -37,6 +43,11 @@
|
|||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Areas\Admin\Views\Product\List.cshtml">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Areas\Admin\Views\_ViewStart.cshtml">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
|
|
@ -146,7 +157,10 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Areas\Admin\Components\_DocumentsGridPartial.cshtml">
|
||||
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Areas\Admin\Views\Order\TestGridComponent.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Areas\Admin\Views\Order\ShippingDocumentGridComponent.cshtml">
|
||||
|
|
@ -605,6 +619,9 @@
|
|||
<None Update="Views\ProductAIListWidget.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Views\OrderAttributes.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Views\ProductAttributes.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
|
@ -620,6 +637,10 @@
|
|||
<Content Include="$(OutDir)\System.ServiceModel.Primitives.dll" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Services\InnvoiceApiService.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- This target execute after "Build" target -->
|
||||
<Target Name="NopTarget" AfterTargets="Build">
|
||||
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
|
||||
|
|
|
|||
|
|
@ -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<string> 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<string> 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<string> 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;
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for interacting with InnVoice Invoice API
|
||||
/// API Documentation: https://help.innvoice.hu/hc/hu/articles/360003142839
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all invoices
|
||||
/// Rate limit: 20 times per hour without ID parameter
|
||||
/// </summary>
|
||||
public async Task<List<Invoice>> GetAllInvoicesAsync()
|
||||
{
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice";
|
||||
return await GetInvoicesFromUrlAsync(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get invoice by internal table ID
|
||||
/// </summary>
|
||||
public async Task<Invoice> GetInvoiceByIdAsync(int tableId)
|
||||
{
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice/id/{tableId}";
|
||||
var invoices = await GetInvoicesFromUrlAsync(url);
|
||||
return invoices.Count > 0 ? invoices[0] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get invoice by invoice number
|
||||
/// </summary>
|
||||
public async Task<List<Invoice>> GetInvoiceByNumberAsync(string invoiceNumber)
|
||||
{
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice/invoicenumber/{Uri.EscapeDataString(invoiceNumber)}";
|
||||
return await GetInvoicesFromUrlAsync(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get invoices by creation date
|
||||
/// Format: YYYYMMDD
|
||||
/// </summary>
|
||||
public async Task<List<Invoice>> GetInvoicesByCreationDateAsync(DateTime date)
|
||||
{
|
||||
var dateStr = date.ToString("yyyyMMdd");
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice/created/{dateStr}";
|
||||
return await GetInvoicesFromUrlAsync(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get invoices by fulfillment date
|
||||
/// Format: YYYYMMDD
|
||||
/// </summary>
|
||||
public async Task<List<Invoice>> GetInvoicesByFulfillmentDateAsync(DateTime date)
|
||||
{
|
||||
var dateStr = date.ToString("yyyyMMdd");
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice/fulfillment/{dateStr}";
|
||||
return await GetInvoicesFromUrlAsync(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get invoices by due date
|
||||
/// Format: YYYYMMDD
|
||||
/// </summary>
|
||||
public async Task<List<Invoice>> GetInvoicesByDueDateAsync(DateTime date)
|
||||
{
|
||||
var dateStr = date.ToString("yyyyMMdd");
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice/duedate/{dateStr}";
|
||||
return await GetInvoicesFromUrlAsync(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get invoices by payment date
|
||||
/// Format: YYYYMMDD
|
||||
/// </summary>
|
||||
public async Task<List<Invoice>> GetInvoicesByPaymentDateAsync(DateTime date)
|
||||
{
|
||||
var dateStr = date.ToString("yyyyMMdd");
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice/paymentdate/{dateStr}";
|
||||
return await GetInvoicesFromUrlAsync(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get invoices by customer tax number
|
||||
/// </summary>
|
||||
public async Task<List<Invoice>> GetInvoicesByCustomerTaxNumberAsync(string taxNumber)
|
||||
{
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice/taxnumber/{Uri.EscapeDataString(taxNumber)}";
|
||||
return await GetInvoicesFromUrlAsync(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public async Task<List<Invoice>> GetInvoicesByUpdateTimeAsync(DateTime updateTime)
|
||||
{
|
||||
var timeStr = updateTime.ToString("yyyyMMddHHmmss");
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice/updatedtime/{timeStr}";
|
||||
return await GetInvoicesFromUrlAsync(url);
|
||||
}
|
||||
|
||||
private async Task<List<Invoice>> GetInvoicesFromUrlAsync(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var invoices = JsonSerializer.Deserialize<List<Invoice>>(content, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
return invoices ?? new List<Invoice>();
|
||||
}
|
||||
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<InvoiceItem> Items { get; set; } = new List<InvoiceItem>();
|
||||
|
||||
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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new invoice
|
||||
/// </summary>
|
||||
public async Task<InvoiceCreateResponse> CreateInvoiceAsync(InvoiceCreateRequest request)
|
||||
{
|
||||
var url = $"{_baseUrl}/{_companyName}/invoice";
|
||||
|
||||
var xml = request.ToXml();
|
||||
var content = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing invoice
|
||||
/// </summary>
|
||||
public async Task<InvoiceCreateResponse> 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<string, string>("data", xml),
|
||||
new KeyValuePair<string, string>("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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string?> 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<string?> 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<string> 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<string> 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<string> 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<string?> 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<string> 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<string> 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
@model Nop.Plugin.Misc.FruitBankPlugin.Models.OrderAttributesModel
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-tags"></i>
|
||||
Custom Order Attributes
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-3">
|
||||
<nop-label asp-for="IsMeasurable" />
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<nop-editor asp-for="IsMeasurable" />
|
||||
<span asp-validation-for="IsMeasurable"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-3">
|
||||
<nop-label asp-for="DateOfReceipt" />
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<nop-editor asp-for="DateOfReceipt" />
|
||||
<span asp-validation-for="DateOfReceipt"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="button" id="saveAttributesBtn" class="btn btn-primary">
|
||||
<i class="fa fa-save"></i> Save Attributes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#saveAttributesBtn").click(function () {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "@Url.Action("SaveOrderAttributes", "CustomOrder")",
|
||||
data: {
|
||||
orderId: "@Model.OrderId",
|
||||
isMeasurable: $("#@Html.IdFor(m => m.IsMeasurable)").is(":checked"),
|
||||
pickupDateTimeUtc: $("#@Html.IdFor(m => m.DateOfReceipt)").val(),
|
||||
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
|
||||
},
|
||||
success: function () {
|
||||
alert("Attributes saved successfully");
|
||||
},
|
||||
error: function () {
|
||||
alert("Error saving attributes");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
@* File: Plugins/Nop.Plugin.YourCompany.ProductAttributes/Views/ProductCustomAttributes.cshtml *@
|
||||
|
||||
|
||||
@model Nop.Plugin.Misc.FruitBankPlugin.Models.ProductAttributesModel
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue