2332 lines
108 KiB
C#
2332 lines
108 KiB
C#
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<bool> HasAccessToOrderAsync(Order order)
|
||
{
|
||
return order != null && await HasAccessToOrderAsync(order.Id);
|
||
}
|
||
|
||
protected virtual async Task<bool> 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<IAcLogWriterBase> 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<CustomOrderController>(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<List<OrderDto>> GetAllOrderDtos() => _customOrderSignalREndpoint.GetAllOrderDtos();
|
||
|
||
[NonAction]
|
||
public Task<OrderDto> GetOrderDtoById(int orderId) => _customOrderSignalREndpoint.GetOrderDtoById(orderId);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderDto>> GetPendingOrderDtos() => _customOrderSignalREndpoint.GetPendingOrderDtos();
|
||
|
||
[NonAction]
|
||
public Task<List<OrderDto>> GetPendingOrderDtosForMeasuring(int lastDaysCount) => _customOrderSignalREndpoint.GetPendingOrderDtosForMeasuring(lastDaysCount);
|
||
|
||
[NonAction]
|
||
public Task<OrderDto> StartMeasuring(int orderId, int userId) => _customOrderSignalREndpoint.StartMeasuring(orderId, userId);
|
||
|
||
[NonAction]
|
||
public Task<OrderDto> SetOrderStatusToComplete(int orderId, int revisorId) => _customOrderSignalREndpoint.SetOrderStatusToComplete(orderId, revisorId);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderDto>> GetAllOrderDtoByIds(int[] orderIds) => _customOrderSignalREndpoint.GetAllOrderDtoByIds(orderIds);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderItemDto>> GetAllOrderItemDtos() => _customOrderSignalREndpoint.GetAllOrderItemDtos();
|
||
|
||
[NonAction]
|
||
public Task<List<OrderDto>> GetAllOrderDtoByProductId(int productId) => _customOrderSignalREndpoint.GetAllOrderDtoByProductId(productId);
|
||
|
||
[NonAction]
|
||
public Task<OrderItemDto> GetOrderItemDtoById(int orderItemId) => _customOrderSignalREndpoint.GetOrderItemDtoById(orderItemId);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderItemDto>> GetAllOrderItemDtoByOrderId(int orderId) => _customOrderSignalREndpoint.GetAllOrderItemDtoByOrderId(orderId);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderItemDto>> GetAllOrderItemDtoByProductId(int productId) => _customOrderSignalREndpoint.GetAllOrderItemDtoByProductId(productId);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderItemPallet>> GetAllOrderItemPallets() => _customOrderSignalREndpoint.GetAllOrderItemPallets();
|
||
|
||
[NonAction]
|
||
public Task<OrderItemPallet> GetOrderItemPalletById(int orderItemPalletId) => _customOrderSignalREndpoint.GetOrderItemPalletById(orderItemPalletId);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderItemPallet>> GetAllOrderItemPalletByOrderItemId(int orderItemId) => _customOrderSignalREndpoint.GetAllOrderItemPalletByOrderItemId(orderItemId);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderItemPallet>> GetAllOrderItemPalletByOrderId(int orderId) => _customOrderSignalREndpoint.GetAllOrderItemPalletByOrderId(orderId);
|
||
|
||
[NonAction]
|
||
public Task<List<OrderItemPallet>> GetAllOrderItemPalletByProductId(int productId) => _customOrderSignalREndpoint.GetAllOrderItemPalletByProductId(productId);
|
||
|
||
[NonAction]
|
||
public Task<OrderItemPallet> AddOrUpdateMeasuredOrderItemPallet(OrderItemPallet orderItemPallet) => _customOrderSignalREndpoint.AddOrUpdateMeasuredOrderItemPallet(orderItemPallet);
|
||
|
||
#endregion CustomOrderSignalREndpoint
|
||
|
||
[CheckPermission(StandardPermission.Orders.ORDERS_VIEW)]
|
||
public virtual async Task<IActionResult> List(List<int> orderStatuses = null, List<int> paymentStatuses = null, List<int> 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<IActionResult> 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<OrderProductItem>()
|
||
: Newtonsoft.Json.JsonConvert.DeserializeObject<List<OrderProductItem>>(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, string>(
|
||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<OrderListModelExtended> 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<OrderListModelExtended> 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<IActionResult> OrderList(OrderSearchModel searchModel)
|
||
//{
|
||
// //prepare model
|
||
// var model = await _orderModelFactory.PrepareOrderListModelAsync(searchModel);
|
||
|
||
// 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");
|
||
|
||
//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, string>(
|
||
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<IActionResult> 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<IActionResult> 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<List<OrderProductItem>>(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<TOrderProductItem>(Order order, IReadOnlyList<TOrderProductItem> 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<OrderItem> CreateOrderItem<TOrderProductItem>(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
|
||
{
|
||
/// <summary>
|
||
/// ProductId
|
||
/// </summary>
|
||
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
|
||
{
|
||
///// <summary>
|
||
///// ProductId
|
||
///// </summary>
|
||
//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<string, object> 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
|
||
// };
|
||
//}
|
||
|
||
|
||
/// <summary>
|
||
/// Add a note to an order that will be displayed in the external application
|
||
/// </summary>
|
||
[HttpPost]
|
||
public async Task<IActionResult> 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<string, object> 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<IActionResult> CustomerSearchAutoComplete(string term)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
|
||
return Json(new List<object>());
|
||
|
||
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<object>();
|
||
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<IActionResult> ProductSearchAutoComplete(string term)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
|
||
return Json(new List<object>());
|
||
|
||
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<object>();
|
||
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<IActionResult> PreorderProductSearchAutoComplete(string term)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
|
||
return Json(new List<object>());
|
||
|
||
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<object>());
|
||
|
||
// 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<object>());
|
||
|
||
var productDtosById = await _dbContext.ProductDtos
|
||
.GetAllByIds(inWindow.Select(p => p.Id))
|
||
.ToDictionaryAsync(k => k.Id, v => v);
|
||
|
||
var result = new List<object>();
|
||
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<IActionResult> ProductSearchUnfilteredAutoComplete(string term)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
|
||
return Json(new List<object>());
|
||
|
||
const int maxResults = 30;
|
||
|
||
var products = await _productService.SearchProductsAsync(
|
||
keywords: term,
|
||
pageIndex: 0,
|
||
pageSize: maxResults);
|
||
|
||
var result = new List<object>();
|
||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> ExportXmlSelected(string selectedIds)
|
||
{
|
||
var orders = new List<Order>();
|
||
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<IActionResult> 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<IActionResult> ExportExcelSelected(string selectedIds)
|
||
{
|
||
var orders = new List<Order>();
|
||
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<List<AddProductModel>>(); //JsonConvert.DeserializeObject<List<AddProductModel>>(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<IActionResult> 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<OrderItemDto> 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<OrderItem>();
|
||
|
||
// 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
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>
|
||
/// Returns the new FruitBank order list view (replaces the default NopCommerce grid).
|
||
/// </summary>
|
||
[CheckPermission(StandardPermission.Orders.ORDERS_VIEW)]
|
||
public async Task<IActionResult> NewList(
|
||
List<int> orderStatuses = null,
|
||
List<int> paymentStatuses = null,
|
||
List<int> 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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// DataTables server-side endpoint for the FruitBank order grid.
|
||
/// Handles NopCommerce base filters + FruitBank-specific filters + per-column search + sort + pagination.
|
||
/// </summary>
|
||
[HttpPost]
|
||
[CheckPermission(StandardPermission.Orders.ORDERS_VIEW)]
|
||
public async Task<IActionResult> 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<string, string>(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<object>() });
|
||
}
|
||
|
||
_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<int> 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<object>() });
|
||
}
|
||
|
||
_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<OrderDto> 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<object>() });
|
||
}
|
||
|
||
_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;
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// Inline-edit save endpoint. Currently supports DateOfReceipt.
|
||
/// </summary>
|
||
[HttpPost]
|
||
[CheckPermission(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)]
|
||
public async Task<IActionResult> 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, string>(order.Id, nameof(IOrderDto.DateOfReceipt), null, _storeContext.GetCurrentStore().Id);
|
||
await _fruitBankAttributeService.DeleteGenericAttributeAsync<Order>(order.Id, nameof(IOrderDto.DateOfReceipt));
|
||
//await _genericAttributeService.SaveAttributeAsync<DateTime?>(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<DateTime?> 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, string>(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 });
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
|