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 _orderRepository; private readonly IRepository _customerRepository; private readonly CustomerCreditDbTable _customerCreditDbTable; private readonly IPermissionService _permissionService; private readonly ILocalizationService _localizationService; public CustomerCreditController( ICustomerCreditService customerCreditService, ICustomerService customerService, IRepository orderRepository, IRepository 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 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 CustomerCreditList() { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Json(new { draw = 1, recordsTotal = 0, recordsFiltered = 0, data = Array.Empty() }); _ = 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 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 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 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 }); } }