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

286 lines
13 KiB
C#

using FruitBank.Common.Entities;
using Microsoft.AspNetCore.Mvc;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using Nop.Data;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Plugin.Misc.FruitBankPlugin.Services;
using Nop.Services.Customers;
using Nop.Services.Localization;
using Nop.Services.Security;
using Nop.Web.Framework;
using Nop.Web.Framework.Controllers;
using Nop.Web.Framework.Mvc.Filters;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers;
[AuthorizeAdmin]
[Area(AreaNames.ADMIN)]
public class CustomerCreditController : BasePluginController
{
private readonly ICustomerCreditService _customerCreditService;
private readonly ICustomerService _customerService;
private readonly IRepository<Order> _orderRepository;
private readonly IRepository<Customer> _customerRepository;
private readonly CustomerCreditDbTable _customerCreditDbTable;
private readonly IPermissionService _permissionService;
private readonly ILocalizationService _localizationService;
public CustomerCreditController(
ICustomerCreditService customerCreditService,
ICustomerService customerService,
IRepository<Order> orderRepository,
IRepository<Customer> customerRepository,
CustomerCreditDbTable customerCreditDbTable,
IPermissionService permissionService,
ILocalizationService localizationService)
{
_customerCreditService = customerCreditService;
_customerService = customerService;
_orderRepository = orderRepository;
_customerRepository = customerRepository;
_customerCreditDbTable = customerCreditDbTable;
_permissionService = permissionService;
_localizationService = localizationService;
}
// ── LIST PAGE ─────────────────────────────────────────────────────────────
[HttpGet]
[Route("Admin/CustomerCredit/List")]
public async Task<IActionResult> List()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/CustomerCredit/List.cshtml");
}
// ── DATATABLES SERVER-SIDE ENDPOINT ──────────────────────────────────────
[HttpPost]
[Route("Admin/CustomerCredit/CustomerCreditList")]
public async Task<IActionResult> CustomerCreditList()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Json(new { draw = 1, recordsTotal = 0, recordsFiltered = 0, data = Array.Empty<object>() });
_ = 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 ? 25 : Math.Min(length, 500);
_ = 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() ?? "OutstandingBalance";
var globalSearch = Request.Form["search[value]"].FirstOrDefault()?.Trim() ?? "";
// 1. Customers — single query
var customers = await _customerRepository.Table
.Where(c => !c.Deleted && c.Active && c.Email != null)
.Select(c => new { c.Id, c.Email, c.FirstName, c.LastName })
.ToListAsync();
// 2. Credit records — single query
var credits = await _customerCreditDbTable.GetAll().ToListAsync();
var creditByCustomer = credits.ToDictionary(x => x.CustomerId);
// 3. Outstanding balances — single grouped query, no N+1
var outstandingByCustomer = await _orderRepository.Table
.Where(o =>
o.OrderStatusId != (int)OrderStatus.Cancelled &&
(o.PaymentStatusId == (int)PaymentStatus.Pending ||
o.PaymentStatusId == (int)PaymentStatus.PartiallyRefunded))
.GroupBy(o => o.CustomerId)
.Select(g => new { CustomerId = g.Key, Total = g.Sum(o => (decimal?)o.OrderTotal) ?? 0m })
.ToListAsync();
var outstandingDict = outstandingByCustomer.ToDictionary(x => x.CustomerId, x => x.Total);
// 4. Build rows
var rows = customers.Select(c =>
{
creditByCustomer.TryGetValue(c.Id, out var credit);
outstandingDict.TryGetValue(c.Id, out var outstanding);
var hasLimit = credit != null;
var remaining = hasLimit ? credit!.CreditLimit - outstanding : (decimal?)null;
return new CustomerCreditListRow
{
CustomerId = c.Id,
CustomerEmail = c.Email ?? string.Empty,
CustomerName = $"{c.FirstName} {c.LastName}".Trim(),
HasCreditLimit = hasLimit,
CreditLimit = credit?.CreditLimit ?? 0m,
OutstandingBalance = outstanding,
RemainingCredit = remaining,
Comment = credit?.Comment
};
}).ToList();
int recordsTotal = rows.Count;
// 5. Global search
if (!string.IsNullOrWhiteSpace(globalSearch))
{
rows = rows.Where(r =>
r.CustomerName.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) ||
r.CustomerEmail.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) ||
(r.Comment?.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) ?? false)
).ToList();
}
int recordsFiltered = rows.Count;
// 6. Sort
bool asc = sortDir == "asc";
rows = sortColName.ToLowerInvariant() switch
{
"customername" => asc ? rows.OrderBy(r => r.CustomerName).ToList() : rows.OrderByDescending(r => r.CustomerName).ToList(),
"customeremail" => asc ? rows.OrderBy(r => r.CustomerEmail).ToList() : rows.OrderByDescending(r => r.CustomerEmail).ToList(),
"creditlimit" => asc ? rows.OrderBy(r => r.CreditLimit).ToList() : rows.OrderByDescending(r => r.CreditLimit).ToList(),
"outstandingbalance" => asc ? rows.OrderBy(r => r.OutstandingBalance).ToList() : rows.OrderByDescending(r => r.OutstandingBalance).ToList(),
"remainingcredit" => asc ? rows.OrderBy(r => r.RemainingCredit ?? decimal.MaxValue).ToList() : rows.OrderByDescending(r => r.RemainingCredit ?? decimal.MinValue).ToList(),
_ => rows.OrderByDescending(r => r.OutstandingBalance).ToList()
};
// 7. Paginate
var page = rows.Skip(start).Take(length).ToList();
return Json(new { draw, recordsTotal, recordsFiltered, data = page });
}
// ── INLINE EDIT: CREDIT LIMIT ─────────────────────────────────────────────
[HttpPost]
[Route("Admin/CustomerCredit/UpdateCreditLimit")]
public async Task<IActionResult> UpdateCreditLimit(int customerId, string? creditLimit, bool removeLimit, string? comment)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Json(new { success = false, error = "Access denied" });
try
{
var existing = await _customerCreditService.GetByCustomerIdAsync(customerId);
var outstanding = await _customerCreditService.GetOutstandingBalanceAsync(customerId);
// Empty input or explicit removeLimit flag → delete record = unlimited
if (removeLimit || string.IsNullOrWhiteSpace(creditLimit))
{
if (existing != null)
await _customerCreditService.DeleteAsync(existing);
return Json(new
{
success = true,
hasLimit = false,
creditLimit = (decimal?)null,
outstanding,
remaining = (decimal?)null
});
}
// Parse the value (JS sends invariant decimal)
if (!decimal.TryParse(creditLimit,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out var limit) || limit < 0)
return Json(new { success = false, error = "Érvénytelen összeg" });
var entity = existing ?? new CustomerCredit { CustomerId = customerId };
entity.CreditLimit = limit;
if (comment != null) entity.Comment = comment;
await _customerCreditService.SaveAsync(entity);
return Json(new
{
success = true,
hasLimit = true,
creditLimit = limit,
outstanding,
remaining = limit - outstanding
});
}
catch (Exception ex)
{
return Json(new { success = false, error = ex.Message });
}
}
// ── DETAILS ───────────────────────────────────────────────────────────────
[HttpGet]
[Route("Admin/CustomerCredit/Details/{customerId:int}")]
public async Task<IActionResult> Details(int customerId)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
var customer = await _customerService.GetCustomerByIdAsync(customerId);
if (customer == null)
return NotFound();
var credit = await _customerCreditService.GetByCustomerIdAsync(customerId);
var outstanding = await _customerCreditService.GetOutstandingBalanceAsync(customerId);
var unpaidOrders = await _orderRepository.Table
.Where(o =>
o.CustomerId == customerId &&
o.OrderStatusId != (int)OrderStatus.Cancelled &&
(o.PaymentStatusId == (int)PaymentStatus.Pending ||
o.PaymentStatusId == (int)PaymentStatus.PartiallyRefunded))
.OrderByDescending(o => o.CreatedOnUtc)
.ToListAsync();
var model = new CustomerCreditModel
{
CustomerId = customerId,
CustomerEmail = customer.Email,
CustomerName = $"{customer.FirstName} {customer.LastName}".Trim(),
CreditId = credit?.Id ?? 0,
CreditLimit = credit?.CreditLimit ?? 0m,
Comment = credit?.Comment,
OutstandingBalance = outstanding,
RemainingCredit = credit != null ? credit.CreditLimit - outstanding : (decimal?)null,
HasCreditLimit = credit != null,
UnpaidOrders = unpaidOrders.Select(o => new CustomerCreditOrderRow
{
OrderId = o.Id,
OrderTotal = o.OrderTotal,
CreatedOnUtc = o.CreatedOnUtc,
OrderStatus = o.OrderStatus.ToString(),
PaymentStatus = o.PaymentStatus.ToString()
}).ToList()
};
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/CustomerCredit/Details.cshtml", model);
}
// ── SAVE (from Details page) ──────────────────────────────────────────────
[HttpPost]
[Route("Admin/CustomerCredit/Save")]
public async Task<IActionResult> Save(CustomerCreditModel model)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
if (!ModelState.IsValid)
return RedirectToAction("Details", new { customerId = model.CustomerId });
var entity = model.CreditId > 0
? await _customerCreditService.GetByCustomerIdAsync(model.CustomerId) ?? new CustomerCredit()
: new CustomerCredit();
entity.CustomerId = model.CustomerId;
entity.CreditLimit = model.CreditLimit;
entity.Comment = model.Comment;
await _customerCreditService.SaveAsync(entity);
return RedirectToAction("Details", new { customerId = model.CustomerId });
}
}