286 lines
13 KiB
C#
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 });
|
|
}
|
|
}
|