using AyCode.Core.Extensions; using AyCode.Core.Loggers; using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using AyCode.Utils.Extensions; using DocumentFormat.OpenXml.Spreadsheet; using FluentMigrator.Runner.Generators.Base; using FruitBank.Common.Dtos; using FruitBank.Common.Entities; using FruitBank.Common.Enums; using FruitBank.Common.Interfaces; using FruitBank.Common.Server; using FruitBank.Common.Server.Interfaces; using FruitBank.Common.Server.Services.SignalRs; using FruitBank.Common.SignalRs; using Mango.Nop.Core.Extensions; using Mango.Nop.Core.Loggers; using MessagePack.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Newtonsoft.Json; using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Customers; using Nop.Core.Domain.Orders; using Nop.Core.Domain.Payments; using Nop.Core.Domain.Shipping; using Nop.Core.Domain.Stores; using Nop.Core.Domain.Tax; using Nop.Core.Events; using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.Order; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Factories; using Nop.Plugin.Misc.FruitBankPlugin.Models.Orders; using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Catalog; using Nop.Services.Common; using Nop.Services.Customers; using Nop.Services.ExportImport; using Nop.Services.Helpers; using Nop.Services.Localization; using Nop.Services.Logging; using Nop.Services.Messages; using Nop.Services.Orders; using Nop.Services.Payments; using Nop.Services.Plugins; using Nop.Services.Security; using Nop.Services.Tax; using Nop.Web.Areas.Admin.Controllers; using Nop.Web.Areas.Admin.Factories; using Nop.Web.Areas.Admin.Models.Orders; using Nop.Web.Framework; using Nop.Web.Framework.Controllers; using Nop.Web.Framework.Mvc.Filters; using System.Text; using System.Text.Json.Serialization; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; using static Nop.Services.Security.StandardPermission; namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { [Area(AreaNames.ADMIN)] [AuthorizeAdmin] public class CustomOrderController : BaseAdminController, ICustomOrderSignalREndpointServer { private readonly FruitBankDbContext _dbContext; private readonly SignalRSendToClientService _sendToClient; private readonly IOrderService _orderService; private readonly CustomOrderModelFactory _orderModelFactory; private readonly ICustomOrderSignalREndpointServer _customOrderSignalREndpoint; private readonly IPermissionService _permissionService; //private readonly IGenericAttributeService _genericAttributeService; private readonly FruitBankAttributeService _fruitBankAttributeService; private readonly INotificationService _notificationService; private readonly ICustomerService _customerService; private readonly IProductService _productService; private readonly IStoreContext _storeContext; private readonly IWorkContext _workContext; private readonly IPriceCalculationService _priceCalculationService; protected readonly IEventPublisher _eventPublisher; protected readonly ILocalizationService _localizationService; protected readonly ICustomerActivityService _customerActivityService; protected readonly IExportManager _exportManager; protected readonly IGiftCardService _giftCardService; protected readonly IImportManager _importManager; protected readonly IDateTimeHelper _dateTimeHelper; protected readonly ITaxService _taxService; protected readonly MeasurementService _measurementService; protected readonly IWorkflowMessageService _workflowMessageService; protected readonly FruitBankNotificationService _fruitBankNotificationService; protected readonly IAddressService _addressService; private readonly FruitBankOrderItemService _orderItemService; private static readonly char[] _separator = [',']; // ... other dependencies private readonly Mango.Nop.Core.Loggers.ILogger _logger; protected virtual async ValueTask HasAccessToOrderAsync(Order order) { return order != null && await HasAccessToOrderAsync(order.Id); } protected virtual async Task HasAccessToOrderAsync(int orderId) { if (orderId == 0) return false; var currentVendor = await _workContext.GetCurrentVendorAsync(); if (currentVendor == null) //not a vendor; has access return true; var vendorId = currentVendor.Id; var hasVendorProducts = (await _orderService.GetOrderItemsAsync(orderId, vendorId: vendorId)).Any(); return hasVendorProducts; } public CustomOrderController(FruitBankDbContext fruitBankDbContext, SignalRSendToClientService sendToClient, IOrderService orderService, IPriceCalculationService priceCalculationService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService, //IGenericAttributeService genericAttributeService, FruitBankAttributeService fruitBankAttributeService, INotificationService notificationService, ICustomerService customerService, IProductService productService, IEnumerable logWriters, IStoreContext storeContext, IWorkContext workContext, IEventPublisher eventPublisher, ILocalizationService localizationService, ICustomerActivityService customerActivityService, IExportManager exportManager, IGiftCardService giftCardService, IImportManager importManager, IDateTimeHelper dateTimeHelper, ITaxService taxService, MeasurementService measurementService, IWorkflowMessageService workflowMessageService, FruitBankNotificationService fruitBankNotificationService, IAddressService addressService, FruitBankOrderItemService orderItemService) { _logger = new Logger(logWriters.ToArray()); _dbContext = fruitBankDbContext; _sendToClient = sendToClient; _orderService = orderService; _orderModelFactory = orderModelFactory as CustomOrderModelFactory; _customOrderSignalREndpoint = customOrderSignalREndpoint; _permissionService = permissionService; //_genericAttributeService = genericAttributeService; _fruitBankAttributeService = fruitBankAttributeService; _notificationService = notificationService; _customerService = customerService; _productService = productService; _storeContext = storeContext; _workContext = workContext; _priceCalculationService = priceCalculationService; _eventPublisher = eventPublisher; _localizationService = localizationService; _customerActivityService = customerActivityService; _exportManager = exportManager; _giftCardService = giftCardService; _importManager = importManager; _dateTimeHelper = dateTimeHelper; _taxService = taxService; _measurementService = measurementService; _workflowMessageService = workflowMessageService; _fruitBankNotificationService = fruitBankNotificationService; _addressService = addressService; _orderItemService = orderItemService; // ... initialize other deps } #region CustomOrderSignalREndpoint [NonAction] public Task> GetAllOrderDtos() => _customOrderSignalREndpoint.GetAllOrderDtos(); [NonAction] public Task GetOrderDtoById(int orderId) => _customOrderSignalREndpoint.GetOrderDtoById(orderId); [NonAction] public Task> GetPendingOrderDtos() => _customOrderSignalREndpoint.GetPendingOrderDtos(); [NonAction] public Task> GetPendingOrderDtosForMeasuring(int lastDaysCount) => _customOrderSignalREndpoint.GetPendingOrderDtosForMeasuring(lastDaysCount); [NonAction] public Task StartMeasuring(int orderId, int userId) => _customOrderSignalREndpoint.StartMeasuring(orderId, userId); [NonAction] public Task SetOrderStatusToComplete(int orderId, int revisorId) => _customOrderSignalREndpoint.SetOrderStatusToComplete(orderId, revisorId); [NonAction] public Task> GetAllOrderDtoByIds(int[] orderIds) => _customOrderSignalREndpoint.GetAllOrderDtoByIds(orderIds); [NonAction] public Task> GetAllOrderItemDtos() => _customOrderSignalREndpoint.GetAllOrderItemDtos(); [NonAction] public Task> GetAllOrderDtoByProductId(int productId) => _customOrderSignalREndpoint.GetAllOrderDtoByProductId(productId); [NonAction] public Task GetOrderItemDtoById(int orderItemId) => _customOrderSignalREndpoint.GetOrderItemDtoById(orderItemId); [NonAction] public Task> GetAllOrderItemDtoByOrderId(int orderId) => _customOrderSignalREndpoint.GetAllOrderItemDtoByOrderId(orderId); [NonAction] public Task> GetAllOrderItemDtoByProductId(int productId) => _customOrderSignalREndpoint.GetAllOrderItemDtoByProductId(productId); [NonAction] public Task> GetAllOrderItemPallets() => _customOrderSignalREndpoint.GetAllOrderItemPallets(); [NonAction] public Task GetOrderItemPalletById(int orderItemPalletId) => _customOrderSignalREndpoint.GetOrderItemPalletById(orderItemPalletId); [NonAction] public Task> GetAllOrderItemPalletByOrderItemId(int orderItemId) => _customOrderSignalREndpoint.GetAllOrderItemPalletByOrderItemId(orderItemId); [NonAction] public Task> GetAllOrderItemPalletByOrderId(int orderId) => _customOrderSignalREndpoint.GetAllOrderItemPalletByOrderId(orderId); [NonAction] public Task> GetAllOrderItemPalletByProductId(int productId) => _customOrderSignalREndpoint.GetAllOrderItemPalletByProductId(productId); [NonAction] public Task AddOrUpdateMeasuredOrderItemPallet(OrderItemPallet orderItemPallet) => _customOrderSignalREndpoint.AddOrUpdateMeasuredOrderItemPallet(orderItemPallet); #endregion CustomOrderSignalREndpoint [CheckPermission(StandardPermission.Orders.ORDERS_VIEW)] public virtual async Task List(List orderStatuses = null, List paymentStatuses = null, List shippingStatuses = null) { //prepare model var model = await _orderModelFactory.PrepareOrderSearchModelAsync(new OrderSearchModelExtended { OrderStatusIds = orderStatuses, PaymentStatusIds = paymentStatuses, ShippingStatusIds = shippingStatuses, Length = 50, AvailablePageSizes = "20,50,100,500", SortColumn = "Id", SortColumnDirection = "desc", }); model.SetGridSort("Id", "desc"); model.SetGridPageSize(50, "20,50,100,500"); return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/List.cshtml", model); } [HttpPost] public virtual async Task AdminQuickCreateOrder(int customerId, string orderProductsJson, string deliveryDateTime) { if (!await _permissionService.AuthorizeAsync(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)) return Json(new { success = false, error = "Hozzáférés megtagadva" }); try { var customer = await _customerService.GetCustomerByIdAsync(customerId); if (customer == null) return Json(new { success = false, error = "Az ügyfél nem található" }); var billingAddress = await _customerService.GetCustomerBillingAddressAsync(customer); if (billingAddress == null) { var addresses = await _customerService.GetAddressesByCustomerIdAsync(customer.Id); if (addresses?.Count > 0) { billingAddress = addresses[0]; customer.BillingAddressId = billingAddress.Id; await _customerService.UpdateCustomerAsync(customer); } else return Json(new { success = false, error = "Az ügyfélnek nincs számlázási címe" }); } var orderProducts = string.IsNullOrEmpty(orderProductsJson) ? new List() : Newtonsoft.Json.JsonConvert.DeserializeObject>(orderProductsJson); var store = await _storeContext.GetCurrentStoreAsync(); var admin = await _workContext.GetCurrentCustomerAsync(); var order = new Order { OrderGuid = Guid.NewGuid(), CustomOrderNumber = "", CustomerId = customerId, CustomerLanguageId = customer.LanguageId ?? 2, CustomerTaxDisplayType = Nop.Core.Domain.Tax.TaxDisplayType.IncludingTax, CustomerIp = string.Empty, OrderStatus = Nop.Core.Domain.Orders.OrderStatus.Pending, PaymentStatus = Nop.Core.Domain.Payments.PaymentStatus.Pending, ShippingStatus = Nop.Core.Domain.Shipping.ShippingStatus.ShippingNotRequired, CreatedOnUtc = DateTime.UtcNow, BillingAddressId = customer.BillingAddressId ?? 0, ShippingAddressId = customer.ShippingAddressId, PaymentMethodSystemName = "Payments.CheckMoneyOrder", CustomerCurrencyCode = "HUF", CurrencyRate = 1, OrderTotal = 0, OrderSubtotalInclTax = 0, OrderSubtotalExclTax = 0, OrderSubTotalDiscountInclTax = 0, OrderSubTotalDiscountExclTax = 0, }; var ok = await _dbContext.TransactionSafeAsync(async _ => { await _orderService.InsertOrderAsync(order); order.CustomOrderNumber = order.Id.ToString(); await AddOrderItemsThenUpdateOrder(order, orderProducts, true, customer, store, admin); return true; }); if (!ok) return Json(new { success = false, error = "Rendelés létrehozása meghiúsult" }); if (!string.IsNullOrWhiteSpace(deliveryDateTime) && DateTime.TryParse(deliveryDateTime, out var deliveryDate)) { var formatted = deliveryDate.ToString("MM/dd/yyyy HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture); await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync( order.Id, nameof(IOrderDto.DateOfReceipt), formatted, store.Id); } _logger.Info($"[AdminQuickCreateOrder] Order #{order.Id} for customer #{customerId}"); return Json(new { success = true, orderId = order.Id }); } catch (Exception ex) { _logger.Error($"[AdminQuickCreateOrder] {ex.Message}", ex); return Json(new { success = false, error = ex.Message }); } } [HttpPost] [CheckPermission(StandardPermission.Orders.ORDERS_VIEW)] public async Task OrderList(OrderSearchModelExtended searchModel) { //prepare model //if (searchModel.SortColumn.IsNullOrWhiteSpace()) //{ // searchModel.SortColumn = "Id"; // searchModel.SortColumnDirection = "desc"; //} var orderListModel = await GetOrderListModelByFilter(searchModel); //var orderListModel = new OrderListModel(); var valami = Json(orderListModel); Console.WriteLine(valami); return valami; } [HttpPost, ActionName("List")] [FormValueRequired("go-to-order-by-number")] [CheckPermission(StandardPermission.Orders.ORDERS_VIEW)] public virtual async Task GoToOrderId(OrderSearchModel model) { var order = await _orderService.GetOrderByCustomOrderNumberAsync(model.GoDirectlyToCustomOrderNumber); if (order == null) return await List(); return RedirectToAction("Edit", new { id = order.Id }); } [HttpGet] //[Route("Edit/{id}")] [CheckPermission(StandardPermission.Orders.ORDERS_VIEW)] public virtual async Task Edit(int id) { //try to get an order with the specified id var order = await _orderService.GetOrderByIdAsync(id); if (order == null || order.Deleted) return RedirectToAction("List"); //a vendor does not have access to this functionality if (await _workContext.GetCurrentVendorAsync() != null) return RedirectToAction("List"); //prepare model var model = await _orderModelFactory.PrepareOrderModelExtendedAsync(null, order); return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/Edit.cshtml", model); } //public async Task GetOrderListModelByFilter(OrderSearchModelExtended searchModel) //{ // //return _customOrderService. // var orderListModel = await _orderModelFactory.PrepareOrderListModelExtendedAsync(searchModel); // _logger.Detail($"Total: {orderListModel.RecordsTotal}, Data Count: {orderListModel.Data.Count()}"); // foreach (var item in orderListModel.Data.Take(3)) // { // _logger.Detail($"Order: {item.Id}, {item.CustomOrderNumber}"); // } // return orderListModel; //} public async Task GetOrderListModelByFilter(OrderSearchModelExtended searchModel) { //if (searchModel.SortColumn.IsNullOrWhiteSpace()) { var sortColumnIndex = Request.Form["order[0][column]"].FirstOrDefault(); var sortDirection = Request.Form["order[0][dir]"].FirstOrDefault(); if (!string.IsNullOrEmpty(sortColumnIndex)) { // Get the column name from the column index var columnName = Request.Form[$"columns[{sortColumnIndex}][data]"].FirstOrDefault(); searchModel.SortColumn = columnName; if (int.Parse(sortColumnIndex) > 0) searchModel.SortColumnDirection = sortDirection; // "asc" or "desc" else searchModel.SortColumnDirection = "desc"; } //else //{ // searchModel.SortColumn = "Id"; // searchModel.SortColumnDirection = "desc"; //} } // Get the paginated data var orderListModel = await _orderModelFactory.PrepareOrderListModelExtendedAsync(searchModel); _logger.Detail($"Total: {orderListModel.RecordsTotal}, Data Count: {orderListModel.Data.Count()}"); // Apply sorting if specified if (!string.IsNullOrEmpty(searchModel.SortColumn) && orderListModel.Data.Any()) { var sortedData = orderListModel.Data.AsQueryable(); sortedData = searchModel.SortColumn.ToLowerInvariant() switch { "id" => searchModel.SortColumnDirection == "asc" ? sortedData.OrderBy(o => o.Id) : sortedData.OrderByDescending(o => o.Id), "customercompany" => searchModel.SortColumnDirection == "asc" ? sortedData.OrderBy(o => o.CustomerCompany) : sortedData.OrderByDescending(o => o.CustomerCompany), "customordernumber" => searchModel.SortColumnDirection == "asc" ? sortedData.OrderBy(o => o.CustomOrderNumber) : sortedData.OrderByDescending(o => o.CustomOrderNumber), "ordertotal" => searchModel.SortColumnDirection == "asc" ? sortedData.OrderBy(o => o.OrderTotal) : sortedData.OrderByDescending(o => o.OrderTotal), "createdon" => searchModel.SortColumnDirection == "asc" ? sortedData.OrderBy(o => o.CreatedOn) : sortedData.OrderByDescending(o => o.CreatedOn), "orderstatusid" => searchModel.SortColumnDirection == "asc" ? sortedData.OrderBy(o => o.OrderStatusId) : sortedData.OrderByDescending(o => o.OrderStatusId), "paymentstatusid" => searchModel.SortColumnDirection == "asc" ? sortedData.OrderBy(o => o.PaymentStatusId) : sortedData.OrderByDescending(o => o.PaymentStatusId), "shippingstatusid" => searchModel.SortColumnDirection == "asc" ? sortedData.OrderBy(o => o.ShippingStatusId) : sortedData.OrderByDescending(o => o.ShippingStatusId), _ => sortedData }; orderListModel.Data = sortedData.ToList(); orderListModel.RecordsTotal = orderListModel.Data.Count(); //orderListModel.Draw = searchModel.Draw; Console.WriteLine($"Sorted Data Count: {orderListModel.Data.Count()}"); Console.WriteLine($"Total Records: {orderListModel.RecordsTotal}"); Console.WriteLine($"Filtered Records: {orderListModel.RecordsFiltered}"); Console.WriteLine($"Draw: {orderListModel.Draw}"); _logger.Detail($"Sorted by {searchModel.SortColumn} {searchModel.SortColumnDirection}"); } foreach (var item in orderListModel.Data.Take(3)) { _logger.Detail($"Order: {item.Id}, {item.CustomOrderNumber}"); } return orderListModel; } public virtual IActionResult Test() { // Your custom logic here // This will use your custom List.cshtml view return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/Test.cshtml"); } //[HttpPost] //[CheckPermission(Nop.Services.Security.StandardPermission.Orders.ORDERS_VIEW)] //public virtual async Task OrderList(OrderSearchModel searchModel) //{ // //prepare model // var model = await _orderModelFactory.PrepareOrderListModelAsync(searchModel); // return Json(model); //} [HttpPost] [ValidateAntiForgeryToken] public async Task SaveOrderAttributes(OrderAttributesModel model) { if (!ModelState.IsValid) { // reload order page with errors return RedirectToAction("Edit", "Order", new { id = model.OrderId }); } var order = await _orderService.GetOrderByIdAsync(model.OrderId); if (order == null) return RedirectToAction("List", "Order"); //TODO: A FruitBankAttributeService-t használjuk és akkor az OrderDto lehet lekérni és beadni a SaveAttribute-ba! - J. // store attributes in GenericAttribute table //await _genericAttributeService.SaveAttributeAsync(order, nameof(IMeasurable.IsMeasurable), model.IsMeasurable, _storeContext.GetCurrentStore().Id); //await _genericAttributeService.SaveAttributeAsync(order, nameof(IOrderDto.DateOfReceipt), model.DateOfReceipt, _storeContext.GetCurrentStore().Id); await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync( order.Id, nameof(IOrderDto.DateOfReceipt), model.DateOfReceipt.HasValue ? model.DateOfReceipt.Value.ToString("MM/dd/yyyy HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture) : null, _storeContext.GetCurrentStore().Id); var orderDto = await _dbContext.OrderDtos.GetByIdAsync(model.OrderId, true); await _sendToClient.SendOrderChanged(orderDto); _notificationService.SuccessNotification("Custom attributes saved successfully."); return RedirectToAction("Edit", "Order", new { id = model.OrderId }); } [HttpPost] [ValidateAntiForgeryToken] public async Task AllowRevision(OrderRevisionModel 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"); //MeasurementService.OrderItemMeasuringReset //Todo: ezt orderitiemnként kéne kirakni?? - Á. var valami = await _measurementService.OrderItemMeasuringReset(model.OrderItemId); return RedirectToAction("Edit", "Order", new { id = model.OrderId }); } [HttpPost] //[CheckPermission(StandardPermission.Orders.ORDERS_CREATE)] public virtual async Task Create(int customerId, string orderProductsJson) { if (!await _permissionService.AuthorizeAsync(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)) return AccessDeniedView(); // Validate customer var customer = await _customerService.GetCustomerByIdAsync(customerId); if (customer == null) return RedirectToAction("List"); var billingAddress = await _customerService.GetCustomerBillingAddressAsync(customer); if (billingAddress == null) { //let's see if he has any address at all var addresses = await _customerService.GetAddressesByCustomerIdAsync(customer.Id); if (addresses != null && addresses.Count > 0) { //set the first one as billing billingAddress = addresses[0]; customer.BillingAddressId = billingAddress.Id; await _customerService.UpdateCustomerAsync(customer); } else { //no address at all, cannot create order _logger.Error($"Cannot create order for customer {customer.Id}, no billing address found."); _notificationService.ErrorNotification("Cannot create order for customer, no billing address found. Please create a billing address for the customer first."); return RedirectToAction("List"); } } //var currency = await _workContext.GetWorkingCurrencyAsync(); //customer.CurrencyId = currency.Id; // Parse products var orderProducts = string.IsNullOrEmpty(orderProductsJson) ? [] : JsonConvert.DeserializeObject>(orderProductsJson); // Create order var order = new Order { OrderGuid = Guid.NewGuid(), CustomOrderNumber = "", CustomerId = customerId, CustomerLanguageId = customer.LanguageId ?? 2, CustomerTaxDisplayType = TaxDisplayType.IncludingTax, CustomerIp = string.Empty, OrderStatus = OrderStatus.Pending, PaymentStatus = PaymentStatus.Pending, ShippingStatus = ShippingStatus.ShippingNotRequired, CreatedOnUtc = DateTime.UtcNow, BillingAddressId = customer.BillingAddressId ?? 0, ShippingAddressId = customer.ShippingAddressId, PaymentMethodSystemName = "Payments.CheckMoneyOrder", // Default payment method CustomerCurrencyCode = "HUF", // TODO: GET Default currency - A. CurrencyRate = 1, OrderTotal = 0, OrderSubtotalInclTax = 0, OrderSubtotalExclTax = 0, OrderSubTotalDiscountInclTax = 0, OrderSubTotalDiscountExclTax = 0, }; //var productDtosById = await _dbContext.ProductDtos.GetAllByIds(orderProducts.Select(op => op.Id)).ToDictionaryAsync(p => p.Id, prodDto => prodDto); var store = await _storeContext.GetCurrentStoreAsync(); var admin = await _workContext.GetCurrentCustomerAsync(); var transactionSuccess = await _dbContext.TransactionSafeAsync(async _ => { await _orderService.InsertOrderAsync(order); order.CustomOrderNumber = order.Id.ToString(); await AddOrderItemsThenUpdateOrder(order, orderProducts, true, customer, store, admin); return true; }); if (transactionSuccess) { //var orderDto = await _dbContext.OrderDtos.GetByIdAsync(order.Id, true); //await _sendToClient.SendMeasuringNotification("Módosult a rendelés, mérjétek újra!", orderDto); //var updatedOrder = await _orderService.GetOrderByIdAsync(order.Id); //await _workflowMessageService.SendOrderPlacedCustomerNotificationAsync(order, order.CustomerLanguageId); if(customer.BillingAddressId.HasValue) { //var billingAddress = await _addressService.GetAddressByIdAsync((int)customer.BillingAddressId); if (billingAddress.Email != null) { if (!billingAddress.Email.EndsWith("inval.id")) { var messageResult = await _fruitBankNotificationService.SendOrderPlacedCustomerNotificationAsync(order, order.CustomerLanguageId); if (messageResult.First() != -1) { _notificationService.SuccessNotification("Order placed email sent to customer."); } else { _logger.Warning($"Order placed email was not sent to customer {customer.Id} because of invalid email address: {billingAddress.Email}"); _notificationService.WarningNotification("Order placed email was not sent to customer because of invalid email address."); } } } } return RedirectToAction("Edit", "Order", new { id = order.Id }); } _logger.Error($"(transactionSuccess == false)"); return RedirectToAction("Error", new { id = order.Id }); } private async Task AddOrderItemsThenUpdateOrder(Order order, IReadOnlyList orderProductItems, bool unitPricesIncludeDiscounts, Customer customer = null, Store store = null, Customer admin = null) where TOrderProductItem : IOrderProductItemBase { store ??= await _storeContext.GetCurrentStoreAsync(); admin ??= await _workContext.GetCurrentCustomerAsync(); customer ??= await _workContext.GetCurrentCustomerAsync(); var helperProductDtosByOrderItemId = await _dbContext.ProductDtos.GetAllByIds(orderProductItems.Select(x => x.Id).ToArray()).ToDictionaryAsync(k => k.Id, v => v); foreach (var orderProductItem in orderProductItems) { var product = await _productService.GetProductByIdAsync(orderProductItem.Id); if (product == null) { _logger.Warning($"Product with ID {orderProductItem.Id} not found"); continue; //var errorText = $"product == null; productId: {item.Id};"; //_logger.Error($"{errorText}"); //throw new Exception($"{errorText}"); } //var stockQuantity = await _productService.GetTotalStockQuantityAsync(product); var productDto = helperProductDtosByOrderItemId[orderProductItem.Id]; var isMeasurable = productDto.IsMeasurable; if ((product.StockQuantity + productDto.IncomingQuantity) - orderProductItem.Quantity < 0) { //errorMessage = $"Nem elérhető készleten!"; var errorText = $"((product.StockQuantity + productDto.IncomingQuantity) - item.Quantity < 0); productId: {product.Id}; (product.StockQuantity + productDto.IncomingQuantity) - item.Quantity: {(product.StockQuantity + productDto.IncomingQuantity) - orderProductItem.Quantity}"; _logger.Error($"{errorText}"); throw new Exception($"{errorText}"); } //itt vajon elég ez a vizsgálat, vagy a priceCalculationService.GetFinalPriceAsync-al kéne lekérni a végső árat és azt összehasonlítani? - A. //ha kedvezménye is van, de manuálisan is le van csökkentve az ár, akkor a kedvezményt látja a rendszer, és azt kellene összevetni a bejövő árral... - A. if (orderProductItem.Price != product.Price) { //manual price change unitPricesIncludeDiscounts = false; } else { unitPricesIncludeDiscounts = true; } //itt ha includeDiscounts van, akkor már a beírt ár megy be? var orderItem = await CreateOrderItem(product, order, orderProductItem, isMeasurable, unitPricesIncludeDiscounts, customer, store); _logger.Detail($"Adding order item: ProductId: {orderItem.ProductId}, Quantity: {orderItem.Quantity}, UnitPriceInclTax: {orderItem.UnitPriceInclTax}, UnitPriceExclTax: {orderItem.UnitPriceExclTax}, PriceInclTax: {orderItem.PriceInclTax}, PriceExclTax: {orderItem.PriceExclTax}"); await _orderService.InsertOrderItemAsync(orderItem); await _productService.AdjustInventoryAsync(product, -orderItem.Quantity, orderItem.AttributesXml, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.PlaceOrder"), order.Id)); // Use the order item values directly — these already reflect any manual // price override from CreateOrderItem, unlike a fresh GetFinalPriceAsync call. order.OrderSubtotalInclTax += orderItem.UnitPriceInclTax * orderItem.Quantity; order.OrderSubtotalExclTax += orderItem.UnitPriceExclTax * orderItem.Quantity; // Discount only applies when price was NOT manually overridden. if (unitPricesIncludeDiscounts) { var priceCalculation = await _priceCalculationService.GetFinalPriceAsync(product, customer, store, includeDiscounts: true); var appliedDiscounts = priceCalculation.appliedDiscountAmount; order.OrderSubTotalDiscountInclTax += appliedDiscounts * orderItem.Quantity; order.OrderSubTotalDiscountExclTax += appliedDiscounts * orderItem.Quantity; } // OrderTotal: add item price only. Shipping and payment fees are NOT added // per item — they are already in the order total from creation and should // not be multiplied by the number of items being added. order.OrderTotal += orderItem.PriceInclTax; } await _orderService.UpdateOrderAsync(order); await InsertOrderNoteAsync(order.Id, false, $"Products added {orderProductItems.Count} item to order by {admin.FirstName} {admin.LastName}, (CustomerId: {admin.Id})"); } private async Task CreateOrderItem(Product product, Order order, TOrderProductItem orderProductItem, bool isMeasurable, bool unitPricesIncludeDiscounts, Customer customer = null, Store store = null) where TOrderProductItem : IOrderProductItemBase { if (product.Id != orderProductItem.Id) throw new Exception($"CustomOrderController->CreateOrderItem; (product.Id != orderProductItem.Id)"); store ??= await _storeContext.GetCurrentStoreAsync(); customer ??= await _workContext.GetCurrentCustomerAsync(); decimal unitPriceInclTaxValue = 0; if (unitPricesIncludeDiscounts) { var priceCalculation = await _priceCalculationService.GetFinalPriceAsync(product, customer, store, includeDiscounts: unitPricesIncludeDiscounts); unitPriceInclTaxValue = priceCalculation.finalPrice; } else { unitPriceInclTaxValue = orderProductItem.Price; } // Calculate tax //var (unitPriceInclTaxValue, _) = await _taxService.GetProductPriceAsync(product, unitPrice, true, customer); var (unitPriceExclTaxValue, _) = await _taxService.GetProductPriceAsync(product, unitPriceInclTaxValue, false, customer); return new OrderItem { OrderId = order.Id, ProductId = orderProductItem.Id, Quantity = orderProductItem.Quantity, OrderItemGuid = Guid.NewGuid(), UnitPriceInclTax = unitPriceInclTaxValue, UnitPriceExclTax = unitPriceExclTaxValue, PriceInclTax = isMeasurable ? 0 : unitPriceInclTaxValue * orderProductItem.Quantity, PriceExclTax = isMeasurable ? 0 : unitPriceExclTaxValue * orderProductItem.Quantity, OriginalProductCost = await _priceCalculationService.GetProductCostAsync(product, null), AttributeDescription = string.Empty, AttributesXml = string.Empty, DiscountAmountInclTax = decimal.Zero, DiscountAmountExclTax = decimal.Zero, DownloadCount = 0, IsDownloadActivated = false, LicenseDownloadId = 0, ItemWeight = product.Weight * orderProductItem.Quantity, RentalStartDateUtc = null, RentalEndDateUtc = null }; } // IOrderProductItemBase is defined in Models/Orders/IOrderProductItemBase.cs // and used as the shared contract across CustomOrderController and FruitBankOrderItemService. public class OrderProductItem : IOrderProductItemBase { /// /// ProductId /// public int Id { get; set; } public string Name { get; set; } public string Sku { get; set; } public int Quantity { get; set; } public decimal Price { get; set; } public override string ToString() { return $"{nameof(OrderProductItem)} [ProductId: {Id}; Name: {Name}; Sku: {Sku}; Quantity: {Quantity}; Price: {Price}]"; } } public class AddProductModel : OrderProductItem { ///// ///// ProductId ///// //public int Id { get; set; } //public string Name { get; set; } //public string Sku { get; set; } //public int Quantity { get; set; } //public decimal Price { get; set; } public int StockQuantity { get; set; } public int IncomingQuantity { get; set; } } //private static OrderItem CreateOrderItem(ProductToAuctionMapping productToAuction, Order order, decimal orderTotal) //{ // return new OrderItem // { // ProductId = productToAuction.ProductId, // OrderId = order.Id, // OrderItemGuid = Guid.NewGuid(), // PriceExclTax = orderTotal, // PriceInclTax = orderTotal, // UnitPriceExclTax = orderTotal, // UnitPriceInclTax = orderTotal, // Quantity = productToAuction.ProductAmount, // }; //} //private static Order CreateOrder(ProductToAuctionMapping productToAuction, decimal orderTotal, Customer customer, Address billingAddress, int storeId, Dictionary customValues) //{ // return new Order // { // BillingAddressId = billingAddress.Id, // CreatedOnUtc = DateTime.UtcNow, // CurrencyRate = 1, // CustomOrderNumber = productToAuction.AuctionId + "/" + productToAuction.SortIndex, // CustomValuesXml = SerializeCustomValuesToXml(customValues), // CustomerCurrencyCode = "HUF", // CustomerId = productToAuction.WinnerCustomerId, // CustomerLanguageId = 2, // CustomerTaxDisplayType = TaxDisplayType.IncludingTax, // OrderGuid = Guid.NewGuid(), // OrderStatus = OrderStatus.Pending, // OrderTotal = orderTotal, // PaymentStatus = PaymentStatus.Pending, // PaymentMethodSystemName = "Payments.CheckMoneyOrder", // ShippingStatus = ShippingStatus.ShippingNotRequired, // StoreId = storeId, // VatNumber = customer.VatNumber, // CustomerIp = customer.LastIpAddress, // OrderSubtotalExclTax = orderTotal, // OrderSubtotalInclTax = orderTotal // }; //} /// /// Add a note to an order that will be displayed in the external application /// [HttpPost] public async Task AddOrderNote(int orderId, string note) { if (!await _permissionService.AuthorizeAsync(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)) return Json(new { success = false, message = "Access denied" }); if (orderId <= 0) { return Json(new { success = false, message = "Invalid order ID" }); } if (string.IsNullOrWhiteSpace(note)) { return Json(new { success = false, message = "Note text is required" }); } try { // Create and insert the order note await InsertOrderNoteAsync(orderId, displayToCustomer: false, note); return Json(new { success = true, message = "Order note added successfully" }); } catch (Exception ex) { return Json(new { success = false, message = $"Error adding order note: {ex.Message}" }); } } private static OrderNote CreateOrderNote(int orderId, bool displayToCustomer, string note) { return new OrderNote { CreatedOnUtc = DateTime.UtcNow,//order.CreatedOnUtc, DisplayToCustomer = displayToCustomer, OrderId = orderId, Note = note }; } public Task InsertOrderNoteAsync(int orderId, bool displayToCustomer, string note) { var orderNote = CreateOrderNote(orderId, displayToCustomer, note); return _orderService.InsertOrderNoteAsync(orderNote); } private static string SerializeCustomValuesToXml(Dictionary sourceDictionary) { ArgumentNullException.ThrowIfNull(sourceDictionary); if (!sourceDictionary.Any()) return null; var ds = new DictionarySerializer(sourceDictionary); var xs = new XmlSerializer(typeof(DictionarySerializer)); using var textWriter = new StringWriter(); using (var xmlWriter = XmlWriter.Create(textWriter)) { xs.Serialize(xmlWriter, ds); } var result = textWriter.ToString(); return result; } [HttpGet] // Change from [HttpPost] to [HttpGet] [CheckPermission(StandardPermission.Customers.CUSTOMERS_VIEW)] public virtual async Task CustomerSearchAutoComplete(string term) { if (string.IsNullOrWhiteSpace(term) || term.Length < 2) return Json(new List()); const int maxResults = 15; // Search by email (contains) var customersByEmail = await _customerService.GetAllCustomersAsync( email: term, pageIndex: 0, pageSize: maxResults); // Search by first name (contains) var customersByFirstName = await _customerService.GetAllCustomersAsync( firstName: term, pageIndex: 0, pageSize: maxResults); // Search by last name (contains) var customersByLastName = await _customerService.GetAllCustomersAsync( lastName: term, pageIndex: 0, pageSize: maxResults); var customersByCompanyName = await _customerService.GetAllCustomersAsync( company: term, pageIndex: 0, pageSize: maxResults); // Combine and deduplicate results var allCustomers = customersByEmail .Union(customersByFirstName) .Union(customersByLastName) .Union(customersByCompanyName) .DistinctBy(c => c.Id) .Take(maxResults) .ToList(); var result = new List(); foreach (var customer in allCustomers) { var fullName = await _customerService.GetCustomerFullNameAsync(customer); var company = customer.Company; if (string.IsNullOrEmpty(fullName)) fullName = "[No name]"; if (string.IsNullOrEmpty(company)) company = "[No company]"; string fullText = $"{company} ({fullName}), {customer.Email}"; //var displayText = !string.IsNullOrEmpty(customer.Email) // ? $"{customer.Email}, {customer.Company} ({fullName})" // : fullName; result.Add(new { label = fullText, value = customer.Id }); } return Json(result); } [HttpGet] [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] public virtual async Task ProductSearchAutoComplete(string term) { if (string.IsNullOrWhiteSpace(term) || term.Length < 2) return Json(new List()); const int maxResults = 30; // Search products by name or SKU var products = await _productService.SearchProductsAsync( keywords: term, pageIndex: 0, pageSize: maxResults); var result = new List(); var productDtosById = await _dbContext.ProductDtos.GetAllByIds(products.Select(p => p.Id)).ToDictionaryAsync(k => k.Id, v => v); foreach (var product in products) { var productDto = productDtosById[product.Id]; if (productDto != null) { if (productDto.AvailableQuantity > 0) { result.Add(new { label = $"{product.Name} [RENDELHETŐ: {productDto.AvailableQuantity} (R:{productDto.StockQuantity}/K:{productDto.IncomingQuantity})] [ÁR: {product.Price}]", value = product.Id, sku = product.Sku, price = product.Price, stockQuantity = product.StockQuantity, availableQuantity = productDto.AvailableQuantity, }); } } } return Json(result); } [HttpGet] [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] public virtual async Task PreorderProductSearchAutoComplete(string term) { if (string.IsNullOrWhiteSpace(term) || term.Length < 2) return Json(new List()); const int maxResults = 30; var today = DateTime.UtcNow.Date; var store = await _storeContext.GetCurrentStoreAsync(); // Load preorder window attributes in two batch queries var gaStart = await _dbContext.GenericAttributes.Table .Where(ga => ga.KeyGroup == nameof(Product) && ga.Key == FruitBankConst.PreorderWindowStart && ga.StoreId == store.Id) .ToListAsync(); var gaEnd = await _dbContext.GenericAttributes.Table .Where(ga => ga.KeyGroup == nameof(Product) && ga.Key == FruitBankConst.PreorderWindowEnd && ga.StoreId == store.Id) .ToListAsync(); var startById = gaStart.ToDictionary(g => g.EntityId, g => g.Value); var endById = gaEnd.ToDictionary(g => g.EntityId, g => g.Value); // Product IDs currently in the preorder window var availableIds = startById.Keys .Intersect(endById.Keys) .Where(id => { DateTime.TryParse(startById[id], out var ws); DateTime.TryParse(endById[id], out var we); return ws.Date <= today && today <= we.Date; }) .ToHashSet(); if (!availableIds.Any()) return Json(new List()); // Search within available products only var products = await _productService.SearchProductsAsync( keywords: term, pageIndex: 0, pageSize: maxResults); var inWindow = products.Where(p => availableIds.Contains(p.Id)).ToList(); if (!inWindow.Any()) return Json(new List()); var productDtosById = await _dbContext.ProductDtos .GetAllByIds(inWindow.Select(p => p.Id)) .ToDictionaryAsync(k => k.Id, v => v); var result = new List(); foreach (var product in inWindow) { productDtosById.TryGetValue(product.Id, out var dto); result.Add(new { label = $"{product.Name} [KÉSZLET: {(product.StockQuantity + (dto?.IncomingQuantity ?? 0))}] [ÁR: {product.Price}]", value = product.Id, sku = product.Sku, price = product.Price, stockQuantity = product.StockQuantity, incomingQuantity = dto?.IncomingQuantity ?? 0 }); } return Json(result); } [HttpGet] [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] public virtual async Task ProductSearchUnfilteredAutoComplete(string term) { if (string.IsNullOrWhiteSpace(term) || term.Length < 2) return Json(new List()); const int maxResults = 30; var products = await _productService.SearchProductsAsync( keywords: term, pageIndex: 0, pageSize: maxResults); var result = new List(); var productDtosById = await _dbContext.ProductDtos.GetAllByIds(products.Select(p => p.Id)).ToDictionaryAsync(k => k.Id, v => v); foreach (var product in products) { var productDto = productDtosById[product.Id]; if (productDto != null) { result.Add(new { label = $"{product.Name} [RENDELHETŐ: {(product.StockQuantity + productDto.IncomingQuantity)}] [ÁR: {product.Price}]", value = product.Id, sku = product.Sku, price = product.Price, stockQuantity = product.StockQuantity, incomingQuantity = productDto.IncomingQuantity, }); } } return Json(result); } //public async Task CreateInvoice(int orderId) //{ // try // { // var order = await _orderService.GetOrderByIdAsync(orderId); // if (order == null) // return Json(new { success = false, message = "Order not found" }); // var billingAddress = await _customerService.GetCustomerBillingAddressAsync(order.Customer); // if (billingAddress == null) // return Json(new { success = false, message = "Billing address not found" }); // var country = await _countryService.GetCountryByAddressAsync(billingAddress); // var countryCode = country?.TwoLetterIsoCode ?? "HU"; // // Create invoice request // var invoiceRequest = new InvoiceCreateRequest // { // VevoNev = $"{billingAddress.FirstName} {billingAddress.LastName}", // VevoIrsz = billingAddress.ZipPostalCode ?? "", // VevoTelep = billingAddress.City ?? "", // VevoOrszag = countryCode, // VevoUtcaHsz = $"{billingAddress.Address1} {billingAddress.Address2}".Trim(), // SzamlatombID = 1, // Configure this based on your setup // SzamlaKelte = DateTime.Now, // TeljesitesKelte = DateTime.Now, // Hatarido = DateTime.Now.AddDays(15), // 15 days payment term // Devizanem = order.CustomerCurrencyCode, // FizetesiMod = order.PaymentMethodSystemName, // Felretett = false, // Proforma = false, // Email = billingAddress.Email, // Telefon = billingAddress.PhoneNumber // }; // // Add order items // var orderItems = await _orderService.GetOrderItemsAsync(order.Id); // foreach (var item in orderItems) // { // var product = await _productService.GetProductByIdAsync(item.ProductId); // invoiceRequest.AddItem(new InvoiceItem // { // TetelNev = product?.Name ?? "Product", // AfaSzoveg = "27%", // Configure VAT rate as needed // Brutto = true, // EgysegAr = item.UnitPriceInclTax, // Mennyiseg = item.Quantity, // MennyisegEgyseg = "db", // CikkSzam = product?.Sku // }); // } // // Create invoice via API // var response = await _innVoiceApiService.CreateInvoiceAsync(invoiceRequest); // if (response.IsSuccess) // { // // TODO: Save invoice details to your database for future reference // // You might want to create a custom table to store: // // - OrderId // // - InnVoice TableId // // - Invoice Number // // - PDF URL // // - Created Date // return Json(new // { // success = true, // message = "Invoice created successfully", // data = new // { // tableId = response.TableId, // invoiceNumber = response.Sorszam, // sorszam = response.Sorszam, // printUrl = response.PrintUrl // } // }); // } // else // { // return Json(new // { // success = false, // message = $"InnVoice API Error: {response.Message}" // }); // } // } // catch (Exception ex) // { // return Json(new // { // success = false, // message = $"Error: {ex.Message}" // }); // } //} //[HttpGet] //public async Task GetInvoiceStatus(int orderId) //{ // try // { // // TODO: Retrieve invoice details from your database // // This is a placeholder - you need to implement actual storage/retrieval // // Example: var invoiceData = await _yourInvoiceService.GetByOrderIdAsync(orderId); // // if (invoiceData != null) // // { // // return Json(new // // { // // success = true, // // data = new // // { // // tableId = invoiceData.TableId, // // invoiceNumber = invoiceData.InvoiceNumber, // // sorszam = invoiceData.InvoiceNumber, // // printUrl = invoiceData.PrintUrl // // } // // }); // // } // return Json(new // { // success = false, // message = "No invoice found for this order" // }); // } // catch (Exception ex) // { // return Json(new // { // success = false, // message = $"Error: {ex.Message}" // }); // } //} //THE REST #region Export / Import [HttpPost, ActionName("ExportXml")] [FormValueRequired("exportxml-all")] [CheckPermission(StandardPermission.Orders.ORDERS_IMPORT_EXPORT)] public virtual async Task ExportXmlAll(OrderSearchModelExtended model) { var startDateValue = model.StartDate == null ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, await _dateTimeHelper.GetCurrentTimeZoneAsync()); var endDateValue = model.EndDate == null ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, await _dateTimeHelper.GetCurrentTimeZoneAsync()).AddDays(1); //a vendor should have access only to his products var currentVendor = await _workContext.GetCurrentVendorAsync(); if (currentVendor != null) { model.VendorId = currentVendor.Id; } var orderStatusIds = model.OrderStatusIds != null && !model.OrderStatusIds.Contains(0) ? model.OrderStatusIds.ToList() : null; var paymentStatusIds = model.PaymentStatusIds != null && !model.PaymentStatusIds.Contains(0) ? model.PaymentStatusIds.ToList() : null; var shippingStatusIds = model.ShippingStatusIds != null && !model.ShippingStatusIds.Contains(0) ? model.ShippingStatusIds.ToList() : null; var filterByProductId = 0; var product = await _productService.GetProductByIdAsync(model.ProductId); if (product != null && (currentVendor == null || product.VendorId == currentVendor.Id)) filterByProductId = model.ProductId; //load orders var orders = await _orderService.SearchOrdersAsync(storeId: model.StoreId, vendorId: model.VendorId, productId: filterByProductId, warehouseId: model.WarehouseId, paymentMethodSystemName: model.PaymentMethodSystemName, createdFromUtc: startDateValue, createdToUtc: endDateValue, osIds: orderStatusIds, psIds: paymentStatusIds, ssIds: shippingStatusIds, billingPhone: model.BillingPhone, billingEmail: model.BillingEmail, billingLastName: model.BillingLastName, billingCountryId: model.BillingCountryId, orderNotes: model.OrderNotes); //ensure that we at least one order selected if (!orders.Any()) { _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Orders.NoOrders")); return RedirectToAction("List"); } try { var xml = await _exportManager.ExportOrdersToXmlAsync(orders); return File(Encoding.UTF8.GetBytes(xml), MimeTypes.ApplicationXml, "orders.xml"); } catch (Exception exc) { await _notificationService.ErrorNotificationAsync(exc); return RedirectToAction("List"); } } [HttpPost] [CheckPermission(StandardPermission.Orders.ORDERS_IMPORT_EXPORT)] public virtual async Task ExportXmlSelected(string selectedIds) { var orders = new List(); if (selectedIds != null) { var ids = selectedIds .Split(_separator, StringSplitOptions.RemoveEmptyEntries) .Select(x => Convert.ToInt32(x)) .ToArray(); orders.AddRange(await (await _orderService.GetOrdersByIdsAsync(ids)) .WhereAwait(HasAccessToOrderAsync).ToListAsync()); } try { var xml = await _exportManager.ExportOrdersToXmlAsync(orders); return File(Encoding.UTF8.GetBytes(xml), MimeTypes.ApplicationXml, "orders.xml"); } catch (Exception exc) { await _notificationService.ErrorNotificationAsync(exc); return RedirectToAction("List"); } } [HttpPost, ActionName("ExportExcel")] [FormValueRequired("exportexcel-all")] [CheckPermission(StandardPermission.Orders.ORDERS_IMPORT_EXPORT)] public virtual async Task ExportExcelAll(OrderSearchModelExtended model) { var startDateValue = model.StartDate == null ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, await _dateTimeHelper.GetCurrentTimeZoneAsync()); var endDateValue = model.EndDate == null ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, await _dateTimeHelper.GetCurrentTimeZoneAsync()).AddDays(1); //a vendor should have access only to his products var currentVendor = await _workContext.GetCurrentVendorAsync(); if (currentVendor != null) { model.VendorId = currentVendor.Id; } var orderStatusIds = model.OrderStatusIds != null && !model.OrderStatusIds.Contains(0) ? model.OrderStatusIds.ToList() : null; var paymentStatusIds = model.PaymentStatusIds != null && !model.PaymentStatusIds.Contains(0) ? model.PaymentStatusIds.ToList() : null; var shippingStatusIds = model.ShippingStatusIds != null && !model.ShippingStatusIds.Contains(0) ? model.ShippingStatusIds.ToList() : null; var filterByProductId = 0; var product = await _productService.GetProductByIdAsync(model.ProductId); if (product != null && (currentVendor == null || product.VendorId == currentVendor.Id)) filterByProductId = model.ProductId; //load orders var orders = await _orderService.SearchOrdersAsync(storeId: model.StoreId, vendorId: model.VendorId, productId: filterByProductId, warehouseId: model.WarehouseId, paymentMethodSystemName: model.PaymentMethodSystemName, createdFromUtc: startDateValue, createdToUtc: endDateValue, osIds: orderStatusIds, psIds: paymentStatusIds, ssIds: shippingStatusIds, billingPhone: model.BillingPhone, billingEmail: model.BillingEmail, billingLastName: model.BillingLastName, billingCountryId: model.BillingCountryId, orderNotes: model.OrderNotes); //ensure that we at least one order selected if (!orders.Any()) { _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Orders.NoOrders")); return RedirectToAction("List"); } try { var bytes = await _exportManager.ExportOrdersToXlsxAsync(orders); return File(bytes, MimeTypes.TextXlsx, "orders.xlsx"); } catch (Exception exc) { await _notificationService.ErrorNotificationAsync(exc); return RedirectToAction("List"); } } [HttpPost] [CheckPermission(StandardPermission.Orders.ORDERS_IMPORT_EXPORT)] public virtual async Task ExportExcelSelected(string selectedIds) { var orders = new List(); if (selectedIds != null) { var ids = selectedIds .Split(_separator, StringSplitOptions.RemoveEmptyEntries) .Select(x => Convert.ToInt32(x)) .ToArray(); orders.AddRange(await (await _orderService.GetOrdersByIdsAsync(ids)).WhereAwait(HasAccessToOrderAsync).ToListAsync()); } try { var bytes = await _exportManager.ExportOrdersToXlsxAsync(orders); return File(bytes, MimeTypes.TextXlsx, "orders.xlsx"); } catch (Exception exc) { await _notificationService.ErrorNotificationAsync(exc); return RedirectToAction("List"); } } [HttpPost] [CheckPermission(StandardPermission.Orders.ORDERS_IMPORT_EXPORT)] public virtual async Task ImportFromXlsx(IFormFile importexcelfile) { //a vendor cannot import orders if (await _workContext.GetCurrentVendorAsync() != null) return AccessDeniedView(); try { if (importexcelfile != null && importexcelfile.Length > 0) { await _importManager.ImportOrdersFromXlsxAsync(importexcelfile.OpenReadStream()); } else { _notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Common.UploadFile")); return RedirectToAction("List"); } _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Orders.Imported")); return RedirectToAction("List"); } catch (Exception exc) { await _notificationService.ErrorNotificationAsync(exc); return RedirectToAction("List"); } } #endregion [HttpPost] [ValidateAntiForgeryToken] public async Task SendOrderNotification(int orderId, string message) { try { if (string.IsNullOrWhiteSpace(message)) { return Json(new { success = false, message = "Az üzenet nem lehet üres" }); } var orderDto = await _dbContext.OrderDtos.GetByIdAsync(orderId, true); await _sendToClient.SendMeasuringNotification(message, orderDto); return Json(new { success = true, message = "Üzenet sikeresen elküldve" }); } catch (Exception ex) { _logger.Error($"Error sending notification for order {orderId}", ex); return Json(new { success = false, message = $"Hiba történt: {ex.Message}" }); } } [HttpPost] [ValidateAntiForgeryToken] [CheckPermission(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)] public async Task SendOrderEmailToCustomer(int orderId) { try { var order = await _orderService.GetOrderByIdAsync(orderId); if (order == null) return Json(new { success = false, message = "Rendelés nem található" }); var sentIds = await _fruitBankNotificationService.SendOrderInfoEmailAsync(order); var sentCount = sentIds?.Count(id => id > 0) ?? 0; if (sentCount > 0) return Json(new { success = true, message = $"Email sikeresen elküldve ({sentCount} címzett)" }); return Json(new { success = false, message = "Az email nem került elküldésre. Ellenőrizze az email sablont és az ügyfél email címét." }); } catch (Exception ex) { _logger.Error($"SendOrderEmailToCustomer error – orderId={orderId}: {ex.Message}", ex); return Json(new { success = false, message = $"Hiba: {ex.Message}" }); } } [HttpPost] [ValidateAntiForgeryToken] [CheckPermission(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)] public async Task FruitBankAddProductToOrder(int orderId, string productsJson) { try { _logger.Info($"AddProductToOrder - OrderId: {orderId}, ProductsJson: {productsJson}"); if (!await _permissionService.AuthorizeAsync(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)) return Json(new { success = false, message = "Access denied" }); if (string.IsNullOrEmpty(productsJson)) return Json(new { success = false, message = "No products data received" }); var order = await _orderService.GetOrderByIdAsync(orderId); if (order == null || order.Deleted) return Json(new { success = false, message = "Order not found" }); // Deserialize products var products = productsJson.JsonTo>(); //JsonConvert.DeserializeObject>(productsJson); if (products == null || products.Count == 0) return Json(new { success = false, message = "No products to add" }); var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId); var store = await _storeContext.GetCurrentStoreAsync(); var admin = await _workContext.GetCurrentCustomerAsync(); string errorMessage = ""; var transactionSuccess = await _dbContext.TransactionSafeAsync(async _ => { await AddOrderItemsThenUpdateOrder(order, products, true, customer, store, admin); return true; }); if (transactionSuccess) { _logger.Info($"Successfully added {products.Count} products to order {orderId}"); //var orderDto = await _dbContext.OrderDtos.GetByIdAsync(order.Id, true); //await _sendToClient.SendMeasuringNotification("Módosult a rendelés, mérjétek újra!", orderDto); return Json(new { success = true, message = "Products added successfully" }); } else { return Json(new { success = false, message = errorMessage }); } } catch (Exception ex) { _logger.Error($"Error adding products to order {orderId}, {ex.Message}"); return Json(new { success = false, message = $"Error: {ex.Message}" }); } } [HttpPost] [ValidateAntiForgeryToken] [CheckPermission(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)] public async Task SplitOrder(int orderId, string mode = "audit", string orderItemIds = "") { try { _logger.Info($"SplitOrder - OrderId: {orderId}, Mode: {mode}, OrderItemIds: {orderItemIds} - STARTED"); var order = await _orderService.GetOrderByIdAsync(orderId); if (order == null || order.Deleted) { _logger.Warning($"SplitOrder - Order {orderId} not found or deleted"); return Json(new { success = false, message = "Rendelés nem található" }); } _logger.Info($"SplitOrder - Order {orderId} found, checking access"); // Check if user has access to this order if (!await HasAccessToOrderAsync(order)) { _logger.Warning($"SplitOrder - No access to order {orderId}"); return Json(new { success = false, message = "Nincs jogosultsága ehhez a rendeléshez" }); } _logger.Info($"SplitOrder - Getting OrderDto for order {orderId}"); var orderDto = await _dbContext.OrderDtos.GetByIdAsync(orderId, true); if (orderDto == null) { _logger.Warning($"SplitOrder - OrderDto not found for order {orderId}"); return Json(new { success = false, message = "OrderDto nem található" }); } // SAFETY CHECK: Don't allow splitting if order is complete/audited if (orderDto.MeasuringStatus == MeasuringStatus.Audited) { _logger.Warning($"SplitOrder - Cannot split audited order {orderId}"); return Json(new { success = false, message = "Ez a rendelés már auditált, nem választható szét!" }); } // REMOVED: NotStarted check - we allow splitting at any stage except Audited // Manual mode is always available, audit mode is controlled by the UI _logger.Info($"SplitOrder - OrderDto found, separating items. Total items: {orderDto.OrderItemDtos.Count}, MeasuringStatus: {orderDto.MeasuringStatus}"); List itemsToMove; if (mode == "manual") { // Manual mode - use provided order item IDs if (string.IsNullOrWhiteSpace(orderItemIds)) { _logger.Warning($"SplitOrder - Manual mode selected but no order item IDs provided"); return Json(new { success = false, message = "Nem lettek termékek kiválasztva" }); } var selectedIds = orderItemIds.Split(',') .Where(id => !string.IsNullOrWhiteSpace(id)) .Select(id => int.Parse(id.Trim())) .ToList(); if (selectedIds.Count == 0) { _logger.Warning($"SplitOrder - No valid order item IDs provided"); return Json(new { success = false, message = "Nem lettek érvényes termékek kiválasztva" }); } if (selectedIds.Count == orderDto.OrderItemDtos.Count) { _logger.Warning($"SplitOrder - All items selected for move"); return Json(new { success = false, message = "Legalább egy terméknek maradnia kell az eredeti rendelésben" }); } itemsToMove = orderDto.OrderItemDtos.Where(oi => selectedIds.Contains(oi.Id)).ToList(); _logger.Info($"SplitOrder - Manual mode: {itemsToMove.Count} items selected to move out of {orderDto.OrderItemDtos.Count}"); } else { // Audit mode - separate by measuring status (started vs not started) // Items with MeasuringStatus > NotStarted stay in original order // Items with MeasuringStatus = NotStarted move to new order var startedItems = orderDto.OrderItemDtos.Where(oi => oi.MeasuringStatus > MeasuringStatus.NotStarted).ToList(); itemsToMove = orderDto.OrderItemDtos.Where(oi => oi.MeasuringStatus == MeasuringStatus.NotStarted).ToList(); _logger.Info($"SplitOrder - Audit mode: Started/Audited items: {startedItems.Count}, Not started items: {itemsToMove.Count}"); if (itemsToMove.Count == 0) { _logger.Warning($"SplitOrder - No not-started items in order {orderId}"); return Json(new { success = false, message = "Nincs nem elindított termék a rendelésben. Szétválasztás nem szükséges." }); } if (startedItems.Count == 0) { _logger.Warning($"SplitOrder - All items are not-started in order {orderId}"); return Json(new { success = false, message = "Minden termék még nem lett elindítva. Használja a kézi módot." }); } } _logger.Info($"SplitOrder - Getting customer, store, and admin"); var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId); var store = await _storeContext.GetCurrentStoreAsync(); var admin = await _workContext.GetCurrentCustomerAsync(); _logger.Info($"SplitOrder - Customer: {customer?.Id}, Store: {store?.Id}, Admin: {admin?.Id}"); _logger.Info($"SplitOrder - Creating new order"); // Create new order for items to move var newOrder = new Order { OrderGuid = Guid.NewGuid(), CustomOrderNumber = "", CustomerId = order.CustomerId, CustomerLanguageId = order.CustomerLanguageId, CustomerTaxDisplayType = order.CustomerTaxDisplayType, CustomerIp = order.CustomerIp, OrderStatus = OrderStatus.Pending, PaymentStatus = PaymentStatus.Pending, ShippingStatus = ShippingStatus.ShippingNotRequired, CreatedOnUtc = DateTime.UtcNow, BillingAddressId = order.BillingAddressId, ShippingAddressId = order.ShippingAddressId, PaymentMethodSystemName = order.PaymentMethodSystemName, CustomerCurrencyCode = order.CustomerCurrencyCode, OrderTotal = 0, OrderSubtotalInclTax = 0, OrderSubtotalExclTax = 0, OrderSubTotalDiscountInclTax = 0, OrderSubTotalDiscountExclTax = 0, }; _logger.Info($"SplitOrder - Inserting new order"); await _orderService.InsertOrderAsync(newOrder); _logger.Info($"SplitOrder - New order inserted with ID: {newOrder.Id}"); newOrder.CustomOrderNumber = newOrder.Id.ToString(); await _orderService.UpdateOrderAsync(newOrder); // Get original order items _logger.Info($"SplitOrder - Getting original order items"); var originalOrderItems = await _orderService.GetOrderItemsAsync(orderId); _logger.Info($"SplitOrder - Found {originalOrderItems.Count} original order items"); var orderItemsToMove = new List(); // Find order items to move based on itemsToMove DTOs foreach (var itemDto in itemsToMove) { var orderItemToMove = originalOrderItems.FirstOrDefault(oi => oi.Id == itemDto.Id); if (orderItemToMove != null) { orderItemsToMove.Add(orderItemToMove); } } _logger.Info($"SplitOrder - Found {orderItemsToMove.Count} items to move"); // Move items to new order foreach (var orderItem in orderItemsToMove) { _logger.Info($"SplitOrder - Processing order item {orderItem.Id}"); var product = await _productService.GetProductByIdAsync(orderItem.ProductId); if (product == null) { _logger.Warning($"Product with ID {orderItem.ProductId} not found during split"); continue; } // Create new order item for new order var newOrderItem = new OrderItem { OrderId = newOrder.Id, ProductId = orderItem.ProductId, Quantity = orderItem.Quantity, OrderItemGuid = Guid.NewGuid(), UnitPriceInclTax = orderItem.UnitPriceInclTax, UnitPriceExclTax = orderItem.UnitPriceExclTax, PriceInclTax = orderItem.PriceInclTax, PriceExclTax = orderItem.PriceExclTax, OriginalProductCost = orderItem.OriginalProductCost, AttributeDescription = orderItem.AttributeDescription, AttributesXml = orderItem.AttributesXml, DiscountAmountInclTax = orderItem.DiscountAmountInclTax, DiscountAmountExclTax = orderItem.DiscountAmountExclTax, DownloadCount = 0, IsDownloadActivated = false, LicenseDownloadId = 0, ItemWeight = orderItem.ItemWeight, RentalStartDateUtc = orderItem.RentalStartDateUtc, RentalEndDateUtc = orderItem.RentalEndDateUtc }; _logger.Info($"SplitOrder - Inserting new order item for product {orderItem.ProductId}"); await _orderService.InsertOrderItemAsync(newOrderItem); // Update new order totals newOrder.OrderSubtotalInclTax += newOrderItem.PriceInclTax; newOrder.OrderSubtotalExclTax += newOrderItem.PriceExclTax; newOrder.OrderTotal += newOrderItem.PriceInclTax; _logger.Info($"SplitOrder - Adjusting inventory for product {orderItem.ProductId}"); // Return inventory to stock (from original order) await _productService.AdjustInventoryAsync( product, orderItem.Quantity, orderItem.AttributesXml, $"Returned from split order #{order.Id}" ); // Remove from stock (for new order) await _productService.AdjustInventoryAsync( product, -orderItem.Quantity, orderItem.AttributesXml, $"Split to new order #{newOrder.Id}" ); _logger.Info($"SplitOrder - Deleting order item {orderItem.Id} from original order"); // Delete from original order await _orderService.DeleteOrderItemAsync(orderItem); // Update original order totals order.OrderSubtotalInclTax -= orderItem.PriceInclTax; order.OrderSubtotalExclTax -= orderItem.PriceExclTax; order.OrderTotal -= orderItem.PriceInclTax; } _logger.Info($"SplitOrder - Updating both orders"); // Update both orders await _orderService.UpdateOrderAsync(newOrder); await _orderService.UpdateOrderAsync(order); _logger.Info($"SplitOrder - Adding order notes"); var splitModeText = mode == "manual" ? "kézi kiválasztással" : "mérési státusz alapján"; // Add notes to both orders await InsertOrderNoteAsync( order.Id, false, $"* Rendelés szétválasztva ({splitModeText}). {orderItemsToMove.Count} termék átkerült a #{newOrder.Id} rendelésbe. Művelet végrehajtója: {admin.FirstName} {admin.LastName} (ID: {admin.Id})" ); await InsertOrderNoteAsync( newOrder.Id, false, $"* Új rendelés létrehozva a #{order.Id} rendelés szétválasztásával ({splitModeText}). {orderItemsToMove.Count} termék. Művelet végrehajtója: {admin.FirstName} {admin.LastName} (ID: {admin.Id})" ); _logger.Info($"Order {orderId} split successfully using {mode} mode. New order created: {newOrder.Id}. Moved {orderItemsToMove.Count} items."); // Send notifications _logger.Info($"SplitOrder - Sending notifications"); var originalOrderDto = await _dbContext.OrderDtos.GetByIdAsync(order.Id, true); var newOrderDto = await _dbContext.OrderDtos.GetByIdAsync(newOrder.Id, true); await _sendToClient.SendOrderChanged(originalOrderDto); await _sendToClient.SendOrderChanged(newOrderDto); _logger.Info($"SplitOrder - COMPLETED SUCCESSFULLY"); return Json(new { success = true, message = "Rendelés sikeresen szétválasztva", newOrderId = newOrder.Id, originalOrderId = order.Id, movedItemsCount = orderItemsToMove.Count }); } catch (Exception ex) { _logger.Error($"Error splitting order {orderId}: {ex.Message}", ex); return Json(new { success = false, message = $"Hiba: {ex.Message}" }); } } // ═══════════════════════════════════════════════════════════════════ // FruitBank Order Grid – new server-side DataTables endpoint // ═══════════════════════════════════════════════════════════════════ /// /// Returns the new FruitBank order list view (replaces the default NopCommerce grid). /// [CheckPermission(StandardPermission.Orders.ORDERS_VIEW)] public async Task NewList( List orderStatuses = null, List paymentStatuses = null, List shippingStatuses = null) { var model = await _orderModelFactory.PrepareOrderSearchModelAsync(new OrderSearchModelExtended { OrderStatusIds = orderStatuses, PaymentStatusIds = paymentStatuses, ShippingStatusIds = shippingStatuses, Length = 50, AvailablePageSizes = "20,50,100,500", SortColumn = "Id", SortColumnDirection = "desc", }); model.SetGridSort("Id", "desc"); model.SetGridPageSize(50, "20,50,100,500"); return View( "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/FruitBankOrderList.cshtml", model); } /// /// DataTables server-side endpoint for the FruitBank order grid. /// Handles NopCommerce base filters + FruitBank-specific filters + per-column search + sort + pagination. /// [HttpPost] [CheckPermission(StandardPermission.Orders.ORDERS_VIEW)] public async Task FruitBankOrderList() { var swTotal = System.Diagnostics.Stopwatch.StartNew(); var sw = System.Diagnostics.Stopwatch.StartNew(); // ── 1. Parse DataTables protocol params ──────────────────────── _ = int.TryParse(Request.Form["draw"].FirstOrDefault(), out int draw); draw = Math.Max(draw, 1); _ = int.TryParse(Request.Form["start"].FirstOrDefault(), out int start); start = Math.Max(start, 0); _ = int.TryParse(Request.Form["length"].FirstOrDefault(), out int length); length = length < 1 ? 50 : Math.Min(length, 500); // Sort column _ = int.TryParse(Request.Form["order[0][column]"].FirstOrDefault(), out int sortColIdx); var sortDir = Request.Form["order[0][dir]"].FirstOrDefault() ?? "desc"; var sortColName = Request.Form[$"columns[{sortColIdx}][data]"].FirstOrDefault() ?? "Id"; // Per-column search values keyed by column data-field name var colSearch = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int ci = 0; Request.Form.ContainsKey($"columns[{ci}][data]"); ci++) { var cData = Request.Form[$"columns[{ci}][data]"].FirstOrDefault(); var cVal = Request.Form[$"columns[{ci}][search][value]"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(cData) && !string.IsNullOrWhiteSpace(cVal)) colSearch[cData] = cVal.Trim(); } // ── 2. Parse custom filter params ───────────────────────────── DateTime? startDate = null, endDate = null; if (DateTime.TryParse(Request.Form["StartDate"].FirstOrDefault(), out var sd)) startDate = sd; if (DateTime.TryParse(Request.Form["EndDate"].FirstOrDefault(), out var ed)) endDate = ed; var orderStatusIds = Request.Form["OrderStatusIds"] .Where(v => int.TryParse(v, out _)).Select(int.Parse).Where(id => id > 0).ToList(); var paymentStatusIds = Request.Form["PaymentStatusIds"] .Where(v => int.TryParse(v, out _)).Select(int.Parse).Where(id => id > 0).ToList(); var shippingStatusIds = Request.Form["ShippingStatusIds"] .Where(v => int.TryParse(v, out _)).Select(int.Parse).Where(id => id > 0).ToList(); var billingCompany = Request.Form["BillingCompany"].FirstOrDefault(); bool? isMeasurableFilter = null; var imStr = Request.Form["IsMeasurable"].FirstOrDefault(); if (imStr == "true") isMeasurableFilter = true; if (imStr == "false") isMeasurableFilter = false; bool? hasInnvoiceFilter = null; var hiStr = Request.Form["HasInnvoiceTechId"].FirstOrDefault(); if (hiStr == "true") hasInnvoiceFilter = true; if (hiStr == "false") hasInnvoiceFilter = false; _logger.Info($"[PERF] FruitBankOrderList – params parsed in {sw.ElapsedMilliseconds} ms"); sw.Restart(); // ── 3. Direct lean query – bypasses the factory N+1 problem ─────── // OrderDtos already has all FruitBank fields + Customer + GenericAttributes. // LinqToDB LoadWith batches relations into 1 query each – 3 queries total // regardless of row count, vs the factory’s ~5 queries per row. int? filterCustomerId = int.TryParse(billingCompany, out var cid) && cid > 0 ? cid : null; // UTC conversion for date filters (same logic as base factory) var currentTz = await _dateTimeHelper.GetCurrentTimeZoneAsync(); DateTime? startUtc = startDate.HasValue ? (DateTime?)_dateTimeHelper.ConvertToUtcTime(startDate.Value, currentTz) : null; DateTime? endUtc = endDate.HasValue ? (DateTime?)_dateTimeHelper.ConvertToUtcTime(endDate.Value, currentTz).AddDays(1) : null; var query = _dbContext.OrderDtos .GetAll(true) // loads GenericAttributes in 1 batch query .Where(o => !o.Deleted); if (startUtc.HasValue) query = query.Where(o => o.CreatedOnUtc >= startUtc.Value); if (endUtc.HasValue) query = query.Where(o => o.CreatedOnUtc <= endUtc.Value); if (filterCustomerId.HasValue) query = query.Where(o => o.CustomerId == filterCustomerId.Value); if (orderStatusIds.Any()) query = query.Where(o => orderStatusIds.Contains(o.OrderStatusId)); if (paymentStatusIds.Any()) query = query.Where(o => paymentStatusIds.Contains(o.PaymentStatusId)); if (shippingStatusIds.Any()) query = query.Where(o => shippingStatusIds.Contains(o.ShippingStatusId)); // Apply sort at DB level bool asc = sortDir == "asc"; query = sortColName.ToLowerInvariant() switch { "customordernumber" => asc ? query.OrderBy(o => o.CustomOrderNumber) : query.OrderByDescending(o => o.CustomOrderNumber), "createdon" => asc ? query.OrderBy(o => o.CreatedOnUtc) : query.OrderByDescending(o => o.CreatedOnUtc), "dateofreceipt" => asc ? query.OrderBy(o => o.DateOfReceipt) : query.OrderByDescending(o => o.DateOfReceipt), "orderstatusid" => asc ? query.OrderBy(o => o.OrderStatusId) : query.OrderByDescending(o => o.OrderStatusId), "measuringstatus" => asc ? query.OrderBy(o => o.MeasuringStatus) : query.OrderByDescending(o => o.MeasuringStatus), "customercompany" => asc ? query.OrderBy(o => o.CustomerId) : query.OrderByDescending(o => o.CustomerId), _ => query.OrderByDescending(o => o.Id) }; // Per-column DB-mappable filters if (colSearch.TryGetValue("CustomOrderNumber", out var coNum) && !string.IsNullOrEmpty(coNum)) query = query.Where(o => o.CustomOrderNumber.Contains(coNum)); if (colSearch.TryGetValue("OrderStatusId", out var osColStr) && int.TryParse(osColStr, out var osColId)) query = query.Where(o => o.OrderStatusId == osColId); if (colSearch.TryGetValue("MeasuringStatus", out var msColStr) && int.TryParse(msColStr, out var msColId)) query = query.Where(o => (int)o.MeasuringStatus == msColId); // IsMeasurable: computed from OrderItemDtos – pre-query the OrderItem table // to get order IDs where any item belongs to a measurable product, then filter SQL var isMeasurableColVal = colSearch.TryGetValue("IsMeasurable", out var imcs) ? imcs : null; bool? effectiveIsMeasurable = isMeasurableFilter; if (isMeasurableColVal != null && bool.TryParse(isMeasurableColVal, out var imcb)) effectiveIsMeasurable = imcb; if (effectiveIsMeasurable.HasValue) { // Get all order IDs where any item has a measurable product var measurableOrderIds = await _dbContext.OrderItemDtos .GetAll(false) .Where(oi => oi.ProductDto != null && oi.ProductDto.IsMeasurable) .Select(oi => oi.OrderId) .Distinct() .ToListAsync(); query = effectiveIsMeasurable.Value ? query.Where(o => measurableOrderIds.Contains(o.Id)) : query.Where(o => !measurableOrderIds.Contains(o.Id)); _logger.Info($"[PERF] FruitBankOrderList – IsMeasurable pre-query: {measurableOrderIds.Count} measurable order IDs"); } // CustomerCompany column search: pre-query Customer table for matching IDs if (colSearch.TryGetValue("CustomerCompany", out var ccColVal) && !string.IsNullOrEmpty(ccColVal)) { var matchingCustomerIds = await _dbContext.Customers.Table .Where(c => c.Company.Contains(ccColVal) || (c.FirstName + " " + c.LastName).Contains(ccColVal)) .Select(c => c.Id) .ToListAsync(); query = query.Where(o => matchingCustomerIds.Contains(o.CustomerId)); _logger.Info($"[PERF] FruitBankOrderList – CustomerCompany pre-query: {matchingCustomerIds.Count} matching customers"); } // COUNT – runs as a simple SELECT COUNT(*) against the filtered set int total; try { total = await query.CountAsync(); } catch (Exception ex) { _logger.Error($"FruitBankOrderList – count error: {ex.Message}", ex); return Json(new { draw, recordsTotal = 0, recordsFiltered = 0, data = Array.Empty() }); } _logger.Info($"[PERF] FruitBankOrderList – COUNT {sw.ElapsedMilliseconds} ms | total: {total}"); sw.Restart(); // Step 1: get just the IDs for the current page (plain SQL, no relations) List pageIds; try { pageIds = await query.Skip(start).Take(length).Select(o => o.Id).ToListAsync(); } catch (Exception ex) { _logger.Error($"FruitBankOrderList – page IDs query error: {ex.Message}", ex); return Json(new { draw, recordsTotal = 0, recordsFiltered = 0, data = Array.Empty() }); } _logger.Info($"[PERF] FruitBankOrderList – page IDs (Skip+Take) {sw.ElapsedMilliseconds} ms | ids: {pageIds.Count}"); sw.Restart(); // Step 2: reload those ~50 rows with only the relations we need. // LoadWith works here because it’s applied to the base table query, not a filtered IQueryable. List rows; try { // GetAllByIds(ids, false) uses GetAll(false) which has LoadWith(GenericAttributes) baked in. // LoadWith on a chained IQueryable is not supported by LinqToDB. rows = await _dbContext.OrderDtos .GetAllByIds(pageIds, true) .ToListAsync(); // Re-sort to match the original query order (IN clause doesn’t guarantee order) rows = pageIds .Select(id => rows.FirstOrDefault(r => r.Id == id)) .Where(r => r != null) .ToList(); } catch (Exception ex) { _logger.Error($"FruitBankOrderList – relations query error: {ex.Message}", ex); return Json(new { draw, recordsTotal = 0, recordsFiltered = 0, data = Array.Empty() }); } _logger.Info($"[PERF] FruitBankOrderList – relations load (GetAll+LoadWith) {sw.ElapsedMilliseconds} ms | rows: {rows.Count}"); sw.Restart(); var userTz = currentTz; // ── 4. Map to lightweight DTO ────────────────────────────────── static string MeasuringStatusLabel(MeasuringStatus s) => s switch { MeasuringStatus.NotStarted => "Nincs elindítva", MeasuringStatus.Started => "Folyamatban", MeasuringStatus.Finnished => "Mérve", MeasuringStatus.Audited => "Lezárva", _ => s.ToString() }; static string OrderStatusLabel(int id) => id switch { 10 => "Függőben", 20 => "Feldolgozás", 30 => "Teljesítve", 40 => "Törölve", _ => id.ToString() }; static string PaymentStatusLabel(int id) => id switch { 10 => "Fizetésre vár", 20 => "Félig fizetve", 30 => "Fizetve", 35 => "Túlfizetve", 40 => "Visszatérítve", _ => id.ToString() }; static string ShippingStatusLabel(int id) => id switch { 10 => "Szállítás nincs", 20 => "Nincs kiszállítva", 25 => "Részben kiszállítva", 30 => "Kiszállítva", _ => id.ToString() }; var dtos = rows.Select(o => { var ga = o.GenericAttributes; var dateOfReceipt = ga?.FirstOrDefault(a => a.Key == "DateOfReceipt")?.Value is string dv && DateTime.TryParse(dv, out var dp) ? dp : (DateTime?)null; var innvoiceTechId = ga?.FirstOrDefault(a => a.Key == "InnVoiceOrderTechId")?.Value; var company = o.Customer != null ? $"{o.Customer.Company} {o.Customer.FirstName}_{o.Customer.LastName}".Trim() : string.Empty; return new Nop.Plugin.Misc.FruitBankPlugin.Models.Orders.FruitBankOrderRowDto { Id = o.Id, CustomOrderNumber = o.CustomOrderNumber, CustomerCompany = company, CustomerId = o.CustomerId, InnvoiceTechId = innvoiceTechId, IsAllOrderItemAvgWeightValid = o.IsAllOrderItemAvgWeightValid, IsMeasurable = o.IsMeasurable, MeasuringStatus = (int)o.MeasuringStatus, MeasuringStatusString = MeasuringStatusLabel(o.MeasuringStatus), DateOfReceipt = dateOfReceipt, OrderStatusId = o.OrderStatusId, OrderStatus = OrderStatusLabel(o.OrderStatusId), PaymentStatusId = o.PaymentStatusId, PaymentStatus = PaymentStatusLabel(o.PaymentStatusId), ShippingStatusId = o.ShippingStatusId, ShippingStatus = ShippingStatusLabel(o.ShippingStatusId), StoreName = string.Empty, // not needed in grid CreatedOn = TimeZoneInfo.ConvertTimeFromUtc(o.CreatedOnUtc, userTz), OrderTotal = !o.IsComplete && o.IsMeasurable ? "kalkuláció alatt..." : $"{o.OrderTotal:N0} Ft" }; }).ToList(); _logger.Info($"[PERF] FruitBankOrderList – DTO mapping {sw.ElapsedMilliseconds} ms"); sw.Restart(); // InnVoice filter is post-query (it’s in GenericAttributes, not a plain column) if (hasInnvoiceFilter.HasValue) dtos = hasInnvoiceFilter.Value ? dtos.Where(o => !string.IsNullOrEmpty(o.InnvoiceTechId)).ToList() : dtos.Where(o => string.IsNullOrEmpty(o.InnvoiceTechId)).ToList(); // InnVoice column-header filter (post-query: stored in GenericAttributes) if (colSearch.TryGetValue("InnvoiceTechId", out var innColVal)) dtos = innColVal == "has" ? dtos.Where(o => !string.IsNullOrEmpty(o.InnvoiceTechId)).ToList() : innColVal == "none" ? dtos.Where(o => string.IsNullOrEmpty(o.InnvoiceTechId)).ToList() : dtos; var result = Json(new { draw, recordsTotal = total, recordsFiltered = total, data = dtos }); _logger.Info($"[PERF] FruitBankOrderList – JSON serialize {sw.ElapsedMilliseconds} ms"); _logger.Info($"[PERF] FruitBankOrderList – TOTAL {swTotal.ElapsedMilliseconds} ms | page: {dtos.Count}"); return result; } /// /// Inline-edit save endpoint. Currently supports DateOfReceipt. /// [HttpPost] [CheckPermission(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)] public async Task UpdateOrderField(int orderId, string field, string value) { try { var order = await _orderService.GetOrderByIdAsync(orderId); if (order == null) return Json(new { success = false, error = "Rendelés nem található" }); switch (field?.ToUpperInvariant()) { case "DATEOFRECEIPT": var dateOdReceiptDateTime = DateTime.TryParse(value, out var dp); if (string.IsNullOrWhiteSpace(value)) { //await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync(order.Id, nameof(IOrderDto.DateOfReceipt), null, _storeContext.GetCurrentStore().Id); await _fruitBankAttributeService.DeleteGenericAttributeAsync(order.Id, nameof(IOrderDto.DateOfReceipt)); //await _genericAttributeService.SaveAttributeAsync(order, "DateOfReceipt", null); return Json(new { success = true, displayValue = (string)null }); } if (DateTime.TryParse(value, out var newDate)) { // Store in the same format that NopCommerce's SaveAttributeAsync uses (MM/dd/yyyy HH:mm:ss invariant) // so OrderDto deserialization in the Blazor app doesn't break. var formattedValue = newDate.ToString("MM/dd/yyyy HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture); await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync(order.Id, nameof(IOrderDto.DateOfReceipt), formattedValue, _storeContext.GetCurrentStore().Id); return Json(new { success = true, displayValue = newDate.ToString("yyyy. MM. dd. HH:mm") }); } return Json(new { success = false, error = "Érvénytelen dátum formátum" }); default: return Json(new { success = false, error = $"Ismeretlen mező: {field}" }); } } catch (Exception ex) { _logger.Error($"UpdateOrderField error – orderId={orderId} field={field}: {ex.Message}", ex); return Json(new { success = false, error = ex.Message }); } } } }