Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomDashboardController.cs

454 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<IActionResult> Index()
{
var customer = await _workContext.GetCurrentCustomerAsync();
var hideCard = await _genericAttributeService.GetAttributeAsync<bool>(customer, NopCustomerDefaults.HideConfigurationStepsAttribute);
var closeCard = await _genericAttributeService.GetAttributeAsync<bool>(customer, NopCustomerDefaults.CloseConfigurationStepsAttribute);
if ((hideCard || closeCard) && 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<string>(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<IActionResult> NopCommerceNewsHideAdv()
{
_adminAreaSettings.HideAdvertisementsOnAdminArea = !_adminAreaSettings.HideAdvertisementsOnAdminArea;
await _settingService.SaveSettingAsync(_adminAreaSettings);
return Content("Setting changed");
}
public virtual async Task<IActionResult> GetPopularSearchTerm()
{
var model = new DataTablesModel();
model = await _homeModelFactory.PreparePopularSearchTermReportModelAsync(model);
return PartialView("Table", model);
}
public virtual async Task<IActionResult> GetBestsellersBriefReportByAmount()
{
var model = new DataTablesModel();
model = await _homeModelFactory.PrepareBestsellersBriefReportByAmountModelAsync(model);
return PartialView("Table", model);
}
public virtual async Task<IActionResult> GetBestsellersBriefReportByQuantity()
{
var model = new DataTablesModel();
model = await _homeModelFactory.PrepareBestsellersBriefReportByQuantityModelAsync(model);
return PartialView("Table", model);
}
public virtual async Task<IActionResult> GetLatestOrders()
{
var model = new DataTablesModel();
model = await _homeModelFactory.PrepareLatestOrdersModelAsync(model);
return PartialView("Table", model);
}
public virtual async Task<IActionResult> GetOrderIncomplete()
{
var model = new DataTablesModel();
model = await _homeModelFactory.PrepareOrderIncompleteModelAsync(model);
return PartialView("Table", model);
}
public virtual async Task<IActionResult> GetOrderAverage()
{
var model = new DataTablesModel();
model = await _homeModelFactory.PrepareOrderAverageModelAsync(model);
return PartialView("Table", model);
}
[HttpGet]
public virtual async Task<IActionResult> GetWelcomeMessage()
{
try
{
var customer = await _workContext.GetCurrentCustomerAsync();
var message = await _aiCalculationService.GetWelcomeMessageAsync(customer);
if (string.IsNullOrWhiteSpace(message))
return Content("<p class=\"lead text-muted\">Nincs kapcsolat az AI szerverrel...</p>");
return Content($"<p class=\"lead\">{System.Net.WebUtility.HtmlEncode(message)}</p>");
}
catch (Exception ex)
{
return Content($"<p class=\"lead text-muted\"><i class=\"fas fa-exclamation-circle mr-2\"></i>{System.Net.WebUtility.HtmlEncode(ex.Message)}</p>");
}
}
/// <summary>
/// Returns all FruitBank dashboard data as a single JSON payload.
/// Called via AJAX after page load — never blocks the dashboard render.
/// </summary>
[HttpGet]
public virtual async Task<IActionResult> 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<PreorderItem>();
// ── 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<int, decimal>();
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<int, string>();
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<object>();
// 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
}
}