454 lines
22 KiB
C#
454 lines
22 KiB
C#
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
|
||
}
|
||
}
|