diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_FruitBankDashboard.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_FruitBankDashboard.cshtml new file mode 100644 index 0000000..6de8dc6 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_FruitBankDashboard.cshtml @@ -0,0 +1,428 @@ +@* FruitBank operational dashboard — loaded via AJAX, never blocks page render *@ + +
+ + Operatív adatok betöltése... +
+ + + + diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_WelcomeMessage.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_WelcomeMessage.cshtml index 074d727..35e3f66 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_WelcomeMessage.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_WelcomeMessage.cshtml @@ -1,62 +1,59 @@ -@using AyCode.Utils.Extensions @using Nop.Core; -@using Nop.Plugin.Misc.FruitBankPlugin.Services @using Nop.Core.Domain.Customers @inject IWorkContext workContext -@inject AICalculationService aiCalculationService + +@{ + var customer = await workContext.GetCurrentCustomerAsync(); +}
-
-

- - Üdvözöljük a Fruit Bank rendszerben! -

-
-
-
-
- @{ - Customer customer = await workContext.GetCurrentCustomerAsync(); +
+

+ + Üdvözöljük a Fruit Bank rendszerben! +

+
+
+
+
+
+

+ + Összefoglaló betöltése... +

+
+

+ Mai dátum: @DateTime.Now.ToString("yyyy. MMMM dd., dddd", new System.Globalization.CultureInfo("hu-HU")) +

+
+
+
+
Mai összefoglaló
+
    +
  • Bejelentkezés ideje: @DateTime.Now.ToString("HH:mm")
  • +
  • Aktív napok: @DateTime.Now.DayOfYear
  • +
  • Szép napot kívánunk!
  • +
+
+
+
+
+
- var welcomeMessage = await aiCalculationService.GetWelcomeMessageAsync(customer); - if (welcomeMessage.IsNullOrWhiteSpace()) - //if(string.IsNullOrWhiteSpace(welcomeMessage)) - { -

Nincs kapcsolat az AI szerverrel...

- } - else - { - var email = customer.Email; -

Ssytem check

-

@welcomeMessage

- } - } -

- Mai dátum: @DateTime.Now.ToString("yyyy. MMMM dd., dddd", new System.Globalization.CultureInfo("hu-HU")) -

- @* -
-
-
-
Gyümölcsök
-

Friss, minőségi gyümölcsök széles választéka

-
-
-
Gyors szállítás
-

Megbízható kiszállítás országszerte

-
-
*@ -
-
-
-
Mai összefoglaló
-
    -
  • Bejelentkezés ideje: @DateTime.Now.ToString("HH:mm")
  • -
  • Aktív napok: @DateTime.Now.DayOfYear
  • -
  • Szép napot kívánunk!
  • -
-
-
-
-
- \ No newline at end of file + diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomDashboardController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomDashboardController.cs index 11871b7..0e7f3c3 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomDashboardController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomDashboardController.cs @@ -1,8 +1,14 @@ -using ExCSS; +using ExCSS; +using FruitBank.Common.Entities; +using FruitBank.Common.Enums; using Microsoft.AspNetCore.Mvc; using Nop.Core; using Nop.Core.Domain.Common; using Nop.Core.Domain.Customers; +using Nop.Core.Domain.Orders; +using Nop.Core.Domain.Payments; +using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; +using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Common; using Nop.Services.Configuration; using Nop.Services.Localization; @@ -29,12 +35,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers protected readonly ISettingService _settingService; protected readonly IGenericAttributeService _genericAttributeService; protected readonly IWorkContext _workContext; + protected readonly AICalculationService _aiCalculationService; + protected readonly FruitBankDbContext _fruitBankDbContext; + protected readonly PreorderDbContext _preorderDbContext; #endregion #region Ctor - public CustomDashboardController(AdminAreaSettings adminAreaSettings, + public CustomDashboardController( + AdminAreaSettings adminAreaSettings, ICommonModelFactory commonModelFactory, IHomeModelFactory homeModelFactory, ILocalizationService localizationService, @@ -42,7 +52,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers IPermissionService permissionService, ISettingService settingService, IGenericAttributeService genericAttributeService, - IWorkContext workContext) + IWorkContext workContext, + AICalculationService aiCalculationService, + FruitBankDbContext fruitBankDbContext, + PreorderDbContext preorderDbContext) { _adminAreaSettings = adminAreaSettings; _commonModelFactory = commonModelFactory; @@ -53,6 +66,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers _settingService = settingService; _workContext = workContext; _genericAttributeService = genericAttributeService; + _aiCalculationService = aiCalculationService; + _fruitBankDbContext = fruitBankDbContext; + _preorderDbContext = preorderDbContext; } #endregion @@ -61,9 +77,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers public virtual async Task Index() { - //display a warning to a store owner if there are some error var customer = await _workContext.GetCurrentCustomerAsync(); - var email = customer.Email; var hideCard = await _genericAttributeService.GetAttributeAsync(customer, NopCustomerDefaults.HideConfigurationStepsAttribute); var closeCard = await _genericAttributeService.GetAttributeAsync(customer, NopCustomerDefaults.CloseConfigurationStepsAttribute); @@ -73,11 +87,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers if (warnings.Any(warning => warning.Level == SystemWarningLevel.Fail || warning.Level == SystemWarningLevel.Warning)) { var locale = await _localizationService.GetResourceAsync("Admin.System.Warnings.Errors"); - _notificationService.WarningNotification(string.Format(locale, Url.Action("Warnings", "Common")), false); //do not encode URLs + _notificationService.WarningNotification(string.Format(locale, Url.Action("Warnings", "Common")), false); } } - //progress of localization var currentLanguage = await _workContext.GetWorkingLanguageAsync(); var progress = await _genericAttributeService.GetAttributeAsync(currentLanguage, NopCommonDefaults.LanguagePackProgressAttribute); if (!string.IsNullOrEmpty(progress)) @@ -87,10 +100,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers await _genericAttributeService.SaveAttributeAsync(currentLanguage, NopCommonDefaults.LanguagePackProgressAttribute, string.Empty); } - //prepare model var model = await _homeModelFactory.PrepareDashboardModelAsync(new DashboardModel()); - - //return View(model); return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Index.cshtml", model); } @@ -99,7 +109,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { _adminAreaSettings.HideAdvertisementsOnAdminArea = !_adminAreaSettings.HideAdvertisementsOnAdminArea; await _settingService.SaveSettingAsync(_adminAreaSettings); - return Content("Setting changed"); } @@ -145,7 +154,300 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return PartialView("Table", model); } + [HttpGet] + public virtual async Task GetWelcomeMessage() + { + try + { + var customer = await _workContext.GetCurrentCustomerAsync(); + var message = await _aiCalculationService.GetWelcomeMessageAsync(customer); + + if (string.IsNullOrWhiteSpace(message)) + return Content("

Nincs kapcsolat az AI szerverrel...

"); + + return Content($"

{System.Net.WebUtility.HtmlEncode(message)}

"); + } + catch (Exception ex) + { + return Content($"

{System.Net.WebUtility.HtmlEncode(ex.Message)}

"); + } + } + + /// + /// Returns all FruitBank dashboard data as a single JSON payload. + /// Called via AJAX after page load — never blocks the dashboard render. + /// + [HttpGet] + public virtual async Task GetDashboardData() + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Orders.ORDERS_VIEW)) + return Forbid(); + + var today = DateTime.Now.Date; + + // ── Batch 1: parallel queries ───────────────────────────────────── + // NOTE: PreorderItems is not a LinqToDB [Association] — never use LoadWith on it. + // Preorders and items are loaded separately and joined in memory below. + var allOrdersTask = _fruitBankDbContext.OrderDtos.GetAll(true).ToListAsync(); + var allCreditsTask = _fruitBankDbContext.CustomerCredits.GetAll().ToListAsync(); + var allPreordersTask = _preorderDbContext.Preorders.GetAll().ToListAsync(); + var unprocessedDocsTask = _fruitBankDbContext.ShippingDocuments.GetAllNotMeasured(true).ToListAsync(); + + await Task.WhenAll(allOrdersTask, allCreditsTask, allPreordersTask, unprocessedDocsTask); + + var allOrders = await allOrdersTask; + var credits = await allCreditsTask; + var unprocessedDocs = await unprocessedDocsTask; + + // Filter pending preorders in memory — LinqToDB cannot translate enum comparisons to SQL + var pendingStatuses = new[] { PreorderStatus.Pending, PreorderStatus.PartiallyFulfilled }; + var pendingPreorders = (await allPreordersTask) + .Where(p => pendingStatuses.Contains(p.Status)) + .ToList(); + + // ── Batch 1b: preorder items for pending preorders only ──────────── + var pendingPreorderIds = pendingPreorders.Select(p => p.Id).ToList(); + var pendingItems = pendingPreorderIds.Any() + ? await _preorderDbContext.PreorderItems.GetAll() + .Where(i => pendingPreorderIds.Contains(i.PreorderId)) + .ToListAsync() + : new List(); + + // ── Today's orders (in-memory filter, same pattern as AICalculationService) + var todaysOrders = allOrders + .Where(o => o.DateOfReceiptOrCreated.Date == today) + .OrderByDescending(o => o.DateOfReceiptOrCreated) + .ToList(); + + // ── Batch 2: unpaid balances for credit customers ────────────────── + var creditCustomerIds = credits.Select(c => c.CustomerId).ToList(); + var unpaidByCustomer = new Dictionary(); + + if (creditCustomerIds.Any()) + { + var unpaid = await _fruitBankDbContext.OrderDtos.GetAll(false) + .Where(o => creditCustomerIds.Contains(o.CustomerId) + && o.OrderStatusId != (int)OrderStatus.Cancelled + && o.PaymentStatusId < (int)PaymentStatus.Paid) + .Select(o => new { o.CustomerId, o.OrderTotal }) + .ToListAsync(); + + unpaidByCustomer = unpaid + .GroupBy(o => o.CustomerId) + .ToDictionary(g => g.Key, g => g.Sum(o => o.OrderTotal)); + } + + // ── Customer name helper: orders cache first, then ID fallback ───── + string CustomerName(int customerId) + { + var fromOrder = allOrders.FirstOrDefault(o => o.CustomerId == customerId)?.Customer; + if (fromOrder != null) + { + if (!string.IsNullOrEmpty(fromOrder.Company)) return fromOrder.Company; + if (!string.IsNullOrEmpty(fromOrder.Email)) return fromOrder.Email; + } + return $"#{customerId}"; + } + + // ── Preorder customer names (may not appear in allOrders) ────────── + var preorderCustomerIds = pendingPreorders + .Select(p => p.CustomerId).Distinct() + .Where(id => allOrders.All(o => o.CustomerId != id)) + .ToList(); + + var preorderCustomerLookup = new Dictionary(); + if (preorderCustomerIds.Any()) + { + var customers = await _preorderDbContext.Customers.Table + .Where(c => preorderCustomerIds.Contains(c.Id)) + .Select(c => new { c.Id, c.Company, c.Email }) + .ToListAsync(); + + foreach (var c in customers) + preorderCustomerLookup[c.Id] = !string.IsNullOrEmpty(c.Company) ? c.Company : c.Email; + } + + string PreorderCustomerName(int customerId) + => preorderCustomerLookup.TryGetValue(customerId, out var name) + ? name + : CustomerName(customerId); + + // ───────────────────────────────────────────────────────────────── + // Section 1: Today's pipeline + // ───────────────────────────────────────────────────────────────── + var todayRows = todaysOrders.Take(20).Select(o => + { + var hasInnVoice = o.GenericAttributes.Any(a => a.Key == "InnVoiceOrderTableId"); + return new + { + id = o.Id, + orderNumber = o.CustomOrderNumber, + company = CustomerName(o.CustomerId), + total = o.OrderTotal, + measuringStatus = o.MeasuringStatus.ToString(), + audited = o.IsAllOrderItemAudited, + hasInnVoice, + orderStatus = o.OrderStatus.ToString(), + dateOfReceipt = o.DateOfReceiptOrCreated.ToString("yyyy.MM.dd HH:mm") + }; + }).ToList(); + + var pipeline = new + { + total = todaysOrders.Count, + measuring = todaysOrders.Count(o => o.MeasuringStatus == MeasuringStatus.Started), + audited = todaysOrders.Count(o => o.IsAllOrderItemAudited), + missingInnVoice = todaysOrders.Count(o => + o.IsAllOrderItemAudited && + !o.GenericAttributes.Any(a => a.Key == "InnVoiceOrderTableId")), + completed = todaysOrders.Count(o => o.OrderStatus == OrderStatus.Complete), + rows = todayRows + }; + + // ───────────────────────────────────────────────────────────────── + // Section 2: Alerts + // ───────────────────────────────────────────────────────────────── + var alerts = new List(); + + // Audited today but InnVoice sync missing and not yet completed + foreach (var o in todaysOrders.Where(o => + o.IsAllOrderItemAudited && + o.OrderStatus != OrderStatus.Complete && + !o.GenericAttributes.Any(a => a.Key == "InnVoiceOrderTableId"))) + { + alerts.Add(new + { + type = "missing_innvoice", + orderId = o.Id, + orderNumber = o.CustomOrderNumber, + company = CustomerName(o.CustomerId), + message = "Auditálva, InnVoice szinkron hiányzik" + }); + } + + // Measuring started but DateOfReceipt is past (stale, stuck in progress) + foreach (var o in allOrders.Where(o => + o.MeasuringStatus == MeasuringStatus.Started && + o.DateOfReceiptOrCreated.Date < today)) + { + alerts.Add(new + { + type = "stale_measuring", + orderId = o.Id, + orderNumber = o.CustomOrderNumber, + company = CustomerName(o.CustomerId), + dateOfReceipt = o.DateOfReceiptOrCreated.ToString("yyyy.MM.dd"), + message = "Régi, befejezetlen mérés" + }); + } + + // Partners over their credit limit + foreach (var credit in credits) + { + var outstanding = unpaidByCustomer.GetValueOrDefault(credit.CustomerId, 0m); + if (outstanding > credit.CreditLimit) + { + alerts.Add(new + { + type = "credit_exceeded", + customerId = credit.CustomerId, + company = CustomerName(credit.CustomerId), + outstanding, + creditLimit = credit.CreditLimit, + message = "Hitelkeret túllépve" + }); + } + } + + // Pending preorders older than 7 days + var sevenDaysAgo = DateTime.UtcNow.AddDays(-7); + foreach (var p in pendingPreorders.Where(p => + p.Status == PreorderStatus.Pending && + p.CreatedOnUtc < sevenDaysAgo)) + { + alerts.Add(new + { + type = "old_preorder", + preorderId = p.Id, + company = PreorderCustomerName(p.CustomerId), + createdAt = p.CreatedOnUtc.ToLocalTime().ToString("yyyy.MM.dd"), + message = "Régi, nyitott előrendelés" + }); + } + + // ───────────────────────────────────────────────────────────────── + // Section 3: Credit status (only partners with a limit set) + // ───────────────────────────────────────────────────────────────── + var creditRows = credits + .Select(credit => + { + var outstanding = unpaidByCustomer.GetValueOrDefault(credit.CustomerId, 0m); + var remaining = credit.CreditLimit - outstanding; + var status = outstanding > credit.CreditLimit + ? "exceeded" + : credit.CreditLimit > 0 && outstanding > credit.CreditLimit * 0.8m + ? "warning" + : "ok"; + return new + { + customerId = credit.CustomerId, + company = CustomerName(credit.CustomerId), + creditLimit = credit.CreditLimit, + outstanding, + remaining, + status + }; + }) + .OrderByDescending(c => c.outstanding) + .ToList(); + + // ───────────────────────────────────────────────────────────────── + // Section 4: Pending preorders (items joined in memory) + // ───────────────────────────────────────────────────────────────── + var preorderRows = pendingPreorders + .OrderBy(p => p.CreatedOnUtc) + .Select(p => + { + var items = pendingItems.Where(i => i.PreorderId == p.Id).ToList(); + return new + { + id = p.Id, + customerId = p.CustomerId, + company = PreorderCustomerName(p.CustomerId), + status = p.Status.ToString(), + createdAt = p.CreatedOnUtc.ToLocalTime().ToString("yyyy.MM.dd"), + itemCount = items.Count, + fulfilledCount = items.Count(i => i.Status == PreorderItemStatus.Fulfilled) + }; + }) + .ToList(); + + // ───────────────────────────────────────────────────────────────── + // Section 5: Unprocessed shipping documents + // ───────────────────────────────────────────────────────────────── + var docRows = unprocessedDocs + .OrderBy(d => d.ShippingDate) + .Select(doc => new + { + id = doc.Id, + partnerName = doc.Partner?.Name ?? "–", + shippingDate = doc.ShippingDate.ToString("yyyy.MM.dd"), + totalItems = doc.ShippingItems?.Count ?? 0, + measuredItems = doc.ShippingItems?.Count(si => si.IsMeasured) ?? 0 + }) + .ToList(); + + // ───────────────────────────────────────────────────────────────── + return Json(new + { + pipeline, + alerts, + creditStatus = creditRows, + pendingPreorders = preorderRows, + unprocessedDocs = docRows + }); + } + #endregion } } - diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs index 8cb8ed6..9afa861 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs @@ -93,6 +93,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers protected readonly IWorkflowMessageService _workflowMessageService; protected readonly FruitBankNotificationService _fruitBankNotificationService; protected readonly IAddressService _addressService; + private readonly FruitBankOrderItemService _orderItemService; private static readonly char[] _separator = [',']; // ... other dependencies @@ -144,7 +145,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers MeasurementService measurementService, IWorkflowMessageService workflowMessageService, FruitBankNotificationService fruitBankNotificationService, - IAddressService addressService) + IAddressService addressService, + FruitBankOrderItemService orderItemService) { _logger = new Logger(logWriters.ToArray()); @@ -176,6 +178,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers _workflowMessageService = workflowMessageService; _fruitBankNotificationService = fruitBankNotificationService; _addressService = addressService; + _orderItemService = orderItemService; // ... initialize other deps @@ -260,7 +263,77 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/List.cshtml", model); } - [HttpPost] + [HttpPost] + public virtual async Task AdminQuickCreateOrder(int customerId, string orderProductsJson, string deliveryDateTime) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)) + return Json(new { success = false, error = "Hozzáférés megtagadva" }); + try + { + var customer = await _customerService.GetCustomerByIdAsync(customerId); + if (customer == null) return Json(new { success = false, error = "Az ügyfél nem található" }); + + var billingAddress = await _customerService.GetCustomerBillingAddressAsync(customer); + if (billingAddress == null) + { + var addresses = await _customerService.GetAddressesByCustomerIdAsync(customer.Id); + if (addresses?.Count > 0) { billingAddress = addresses[0]; customer.BillingAddressId = billingAddress.Id; await _customerService.UpdateCustomerAsync(customer); } + else return Json(new { success = false, error = "Az ügyfélnek nincs számlázási címe" }); + } + + var orderProducts = string.IsNullOrEmpty(orderProductsJson) + ? new List() + : Newtonsoft.Json.JsonConvert.DeserializeObject>(orderProductsJson); + + var store = await _storeContext.GetCurrentStoreAsync(); + var admin = await _workContext.GetCurrentCustomerAsync(); + + var order = new Order + { + OrderGuid = Guid.NewGuid(), CustomOrderNumber = "", CustomerId = customerId, + CustomerLanguageId = customer.LanguageId ?? 2, + CustomerTaxDisplayType = Nop.Core.Domain.Tax.TaxDisplayType.IncludingTax, + CustomerIp = string.Empty, + OrderStatus = Nop.Core.Domain.Orders.OrderStatus.Pending, + PaymentStatus = Nop.Core.Domain.Payments.PaymentStatus.Pending, + ShippingStatus = Nop.Core.Domain.Shipping.ShippingStatus.ShippingNotRequired, + CreatedOnUtc = DateTime.UtcNow, + BillingAddressId = customer.BillingAddressId ?? 0, + ShippingAddressId = customer.ShippingAddressId, + PaymentMethodSystemName = "Payments.CheckMoneyOrder", + CustomerCurrencyCode = "HUF", CurrencyRate = 1, + OrderTotal = 0, OrderSubtotalInclTax = 0, OrderSubtotalExclTax = 0, + OrderSubTotalDiscountInclTax = 0, OrderSubTotalDiscountExclTax = 0, + }; + + var ok = await _dbContext.TransactionSafeAsync(async _ => + { + await _orderService.InsertOrderAsync(order); + order.CustomOrderNumber = order.Id.ToString(); + await AddOrderItemsThenUpdateOrder(order, orderProducts, true, customer, store, admin); + return true; + }); + + if (!ok) return Json(new { success = false, error = "Rendelés létrehozása meghiúsult" }); + + if (!string.IsNullOrWhiteSpace(deliveryDateTime) && DateTime.TryParse(deliveryDateTime, out var deliveryDate)) + { + var formatted = deliveryDate.ToString("MM/dd/yyyy HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture); + await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync( + order.Id, nameof(IOrderDto.DateOfReceipt), formatted, store.Id); + } + + _logger.Info($"[AdminQuickCreateOrder] Order #{order.Id} for customer #{customerId}"); + return Json(new { success = true, orderId = order.Id }); + } + catch (Exception ex) + { + _logger.Error($"[AdminQuickCreateOrder] {ex.Message}", ex); + return Json(new { success = false, error = ex.Message }); + } + } + + [HttpPost] [CheckPermission(StandardPermission.Orders.ORDERS_VIEW)] public async Task OrderList(OrderSearchModelExtended searchModel) { @@ -654,25 +727,24 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers await _orderService.InsertOrderItemAsync(orderItem); await _productService.AdjustInventoryAsync(product, -orderItem.Quantity, orderItem.AttributesXml, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.PlaceOrder"), order.Id)); - var priceCalculation = await _priceCalculationService.GetFinalPriceAsync(product, customer, store, includeDiscounts: true); - var unitPriceInclTaxValue = priceCalculation.finalPrice; + // 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; - var (unitPriceExclTaxValue, _) = await _taxService.GetProductPriceAsync(product, unitPriceInclTaxValue, false, customer); + // 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; + } - order.OrderSubtotalInclTax += unitPriceInclTaxValue * orderItem.Quantity; - order.OrderSubtotalExclTax += unitPriceExclTaxValue * orderItem.Quantity; - - var appliedDiscounts = priceCalculation.appliedDiscountAmount; - var totalDiscountInclTax = appliedDiscounts * orderProductItem.Quantity; - var totalDiscountExclTax = appliedDiscounts * orderProductItem.Quantity; - - order.OrderSubTotalDiscountInclTax += totalDiscountInclTax; - order.OrderSubTotalDiscountExclTax += totalDiscountExclTax; - - //order.OrderTax - //order.TaxRates - - order.OrderTotal += orderItem.PriceInclTax + order.OrderShippingInclTax + order.PaymentMethodAdditionalFeeInclTax; + // 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); @@ -737,15 +809,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers }; } - public interface IOrderProductItemBase - { - /// - /// ProductId - /// - public int Id { get; set; } - public int Quantity { get; set; } - public decimal Price { get; set; } - } + // IOrderProductItemBase is defined in Models/Orders/IOrderProductItemBase.cs + // and used as the shared contract across CustomOrderController and FruitBankOrderItemService. public class OrderProductItem : IOrderProductItemBase { @@ -995,12 +1060,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { result.Add(new { - label = $"{product.Name} [RENDELHETŐ: {(product.StockQuantity + productDto.IncomingQuantity)}] [ÁR: {product.Price}]", + 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, - incomingQuantity = productDto.IncomingQuantity, + availableQuantity = productDto.AvailableQuantity, }); } } diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Index.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Index.cshtml index 2a8b998..e25b253 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Index.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Index.cshtml @@ -41,17 +41,15 @@ @await Html.PartialAsync("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Components/_WelcomeMessage.cshtml") - } - - @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.DashboardNewsAfter, additionalData = Model }) - @if (!Model.IsLoggedInAsVendor && canManageOrders && canManageCustomers && canManageProducts && canManageReturnRequests) - {
- @await Component.InvokeAsync(typeof(CommonStatisticsViewComponent)) + @await Html.PartialAsync("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Components/_FruitBankDashboard.cshtml")
} + + @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.DashboardNewsAfter, additionalData = Model }) + @* CommonStatisticsViewComponent removed — runs GetLowStockProductsAsync full table scan, causes SQL timeout *@ @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.DashboardCommonstatisticsAfter, additionalData = Model }) @if (!Model.IsLoggedInAsVendor && (canManageOrders || canManageCustomers)) { @@ -83,35 +81,16 @@ } @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.DashboardOrderreportsAfter, additionalData = Model }) - @if (!Model.IsLoggedInAsVendor && (canManageOrders || canManageProducts)) + @if (!Model.IsLoggedInAsVendor && canManageOrders) {
- @if (canManageOrders) - { -
- @await Html.PartialAsync("~/Areas/Admin/Views/Home/_LatestOrders.cshtml") -
- } -
- @if (canManageProducts) - { - @await Html.PartialAsync("~/Areas/Admin/Views/Home/_PopularSearchTermsReport.cshtml") - } +
+ @await Html.PartialAsync("~/Areas/Admin/Views/Home/_LatestOrders.cshtml")
} @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.DashboardLatestordersSearchtermsAfter, additionalData = Model }) - @if (canManageOrders) - { -
-
- @await Html.PartialAsync("~/Areas/Admin/Views/Home/_BestsellersBriefReportByQuantity.cshtml") -
-
- @await Html.PartialAsync("~/Areas/Admin/Views/Home/_BestsellersBriefReportByAmount.cshtml") -
-
- } + @* PopularSearchTermsReport and BestsellersBriefReports removed — not relevant to FruitBank warehouse workflow *@ @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.DashboardBottom, additionalData = Model })
diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FruitBankOrderList.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FruitBankOrderList.cshtml index d70c0e0..ebada1f 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FruitBankOrderList.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/FruitBankOrderList.cshtml @@ -591,7 +591,7 @@ $(function () { function addCreateProduct(item) { if (createProducts.find(function (p) { return p.id === item.value; })) return; - createProducts.push({ id: item.value, name: item.label, quantity: 1, price: item.price || 0 }); + createProducts.push({ id: item.value, name: item.label, quantity: 1, price: item.price || 0, maxQuantity: item.availableQuantity || 9999 }); renderCreateProducts(); } @@ -600,10 +600,12 @@ $(function () { if (!createProducts.length) { $('#create-selected-products-section').hide(); return; } $('#create-selected-products-section').show(); createProducts.forEach(function (p, i) { + var maxAttr = p.maxQuantity < 9999 ? ' max="' + p.maxQuantity + '"' : ''; + var maxHint = p.maxQuantity < 9999 ? '
max: ' + p.maxQuantity + ' db' : ''; $body.append( '' + '' + p.name + '' + - '' + + '' + maxHint + '' + '' + '' + '' @@ -612,7 +614,13 @@ $(function () { $('#create-order-products-json').val(JSON.stringify(createProducts)); } - window._fbUpdateQty = function (el) { createProducts[+el.dataset.idx].quantity = +el.value; renderCreateProducts(); }; + window._fbUpdateQty = function (el) { + var idx = +el.dataset.idx, val = +el.value, max = createProducts[idx].maxQuantity || 9999; + if (val > max) { val = max; el.value = max; } + if (val < 1) { val = 1; el.value = 1; } + createProducts[idx].quantity = val; + $('#create-order-products-json').val(JSON.stringify(createProducts)); + }; window._fbUpdatePrice = function (el) { createProducts[+el.dataset.idx].price = +el.value; renderCreateProducts(); }; window._fbRemoveProduct = function (i) { createProducts.splice(i, 1); renderCreateProducts(); }; diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Preorder/List.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Preorder/List.cshtml index e0c1b71..cdfcba8 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Preorder/List.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Preorder/List.cshtml @@ -13,7 +13,7 @@
@@ -21,7 +21,6 @@
- - - +
-@* ── Create Preorder Modal ──────────────────────────────────────────────── *@ +@* ── Modal ─────────────────────────────────────────────────────────────── *@