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; using Nop.Services.Messages; using Nop.Services.Security; using Nop.Web.Areas.Admin.Controllers; using Nop.Web.Areas.Admin.Factories; using Nop.Web.Areas.Admin.Models.Common; using Nop.Web.Areas.Admin.Models.Home; using Nop.Web.Framework.Models.DataTables; namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { public partial class CustomDashboardController : BaseAdminController { #region Fields protected readonly AdminAreaSettings _adminAreaSettings; protected readonly ICommonModelFactory _commonModelFactory; protected readonly IHomeModelFactory _homeModelFactory; protected readonly ILocalizationService _localizationService; protected readonly INotificationService _notificationService; protected readonly IPermissionService _permissionService; 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, ICommonModelFactory commonModelFactory, IHomeModelFactory homeModelFactory, ILocalizationService localizationService, INotificationService notificationService, IPermissionService permissionService, ISettingService settingService, IGenericAttributeService genericAttributeService, IWorkContext workContext, AICalculationService aiCalculationService, FruitBankDbContext fruitBankDbContext, PreorderDbContext preorderDbContext) { _adminAreaSettings = adminAreaSettings; _commonModelFactory = commonModelFactory; _homeModelFactory = homeModelFactory; _localizationService = localizationService; _notificationService = notificationService; _permissionService = permissionService; _settingService = settingService; _workContext = workContext; _genericAttributeService = genericAttributeService; _aiCalculationService = aiCalculationService; _fruitBankDbContext = fruitBankDbContext; _preorderDbContext = preorderDbContext; } #endregion #region Methods public virtual async Task Index() { var customer = await _workContext.GetCurrentCustomerAsync(); var hideCard = await _genericAttributeService.GetAttributeAsync(customer, NopCustomerDefaults.HideConfigurationStepsAttribute); var closeCard = await _genericAttributeService.GetAttributeAsync(customer, NopCustomerDefaults.CloseConfigurationStepsAttribute); if ((hideCard || closeCard) && await _permissionService.AuthorizeAsync(StandardPermission.System.MANAGE_MAINTENANCE)) { var warnings = await _commonModelFactory.PrepareSystemWarningModelsAsync(); 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); } } var currentLanguage = await _workContext.GetWorkingLanguageAsync(); var progress = await _genericAttributeService.GetAttributeAsync(currentLanguage, NopCommonDefaults.LanguagePackProgressAttribute); if (!string.IsNullOrEmpty(progress)) { var locale = await _localizationService.GetResourceAsync("Admin.Configuration.LanguagePackProgressMessage"); _notificationService.SuccessNotification(string.Format(locale, progress, NopLinksDefaults.OfficialSite.Translations), false); await _genericAttributeService.SaveAttributeAsync(currentLanguage, NopCommonDefaults.LanguagePackProgressAttribute, string.Empty); } var model = await _homeModelFactory.PrepareDashboardModelAsync(new DashboardModel()); return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Index.cshtml", model); } [HttpPost] public virtual async Task NopCommerceNewsHideAdv() { _adminAreaSettings.HideAdvertisementsOnAdminArea = !_adminAreaSettings.HideAdvertisementsOnAdminArea; await _settingService.SaveSettingAsync(_adminAreaSettings); return Content("Setting changed"); } public virtual async Task GetPopularSearchTerm() { var model = new DataTablesModel(); model = await _homeModelFactory.PreparePopularSearchTermReportModelAsync(model); return PartialView("Table", model); } public virtual async Task GetBestsellersBriefReportByAmount() { var model = new DataTablesModel(); model = await _homeModelFactory.PrepareBestsellersBriefReportByAmountModelAsync(model); return PartialView("Table", model); } public virtual async Task GetBestsellersBriefReportByQuantity() { var model = new DataTablesModel(); model = await _homeModelFactory.PrepareBestsellersBriefReportByQuantityModelAsync(model); return PartialView("Table", model); } public virtual async Task GetLatestOrders() { var model = new DataTablesModel(); model = await _homeModelFactory.PrepareLatestOrdersModelAsync(model); return PartialView("Table", model); } public virtual async Task GetOrderIncomplete() { var model = new DataTablesModel(); model = await _homeModelFactory.PrepareOrderIncompleteModelAsync(model); return PartialView("Table", model); } public virtual async Task GetOrderAverage() { var model = new DataTablesModel(); model = await _homeModelFactory.PrepareOrderAverageModelAsync(model); 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 } }