CustomerCredit, new order
This commit is contained in:
parent
8e1b3f2a5d
commit
51f546caec
|
|
@ -1759,7 +1759,235 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// FruitBank Order Grid – new server-side DataTables endpoint
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the new FruitBank order list view (replaces the default NopCommerce grid).
|
||||||
|
/// </summary>
|
||||||
|
[CheckPermission(StandardPermission.Orders.ORDERS_VIEW)]
|
||||||
|
public async Task<IActionResult> NewList(
|
||||||
|
List<int> orderStatuses = null,
|
||||||
|
List<int> paymentStatuses = null,
|
||||||
|
List<int> shippingStatuses = null)
|
||||||
|
{
|
||||||
|
var model = await _orderModelFactory.PrepareOrderSearchModelAsync(new OrderSearchModelExtended
|
||||||
|
{
|
||||||
|
OrderStatusIds = orderStatuses,
|
||||||
|
PaymentStatusIds = paymentStatuses,
|
||||||
|
ShippingStatusIds = shippingStatuses,
|
||||||
|
Length = 50,
|
||||||
|
AvailablePageSizes = "20,50,100,500",
|
||||||
|
SortColumn = "Id",
|
||||||
|
SortColumnDirection = "desc",
|
||||||
|
});
|
||||||
|
model.SetGridSort("Id", "desc");
|
||||||
|
model.SetGridPageSize(50, "20,50,100,500");
|
||||||
|
|
||||||
|
return View(
|
||||||
|
"~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/FruitBankOrderList.cshtml",
|
||||||
|
model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DataTables server-side endpoint for the FruitBank order grid.
|
||||||
|
/// Handles NopCommerce base filters + FruitBank-specific filters + per-column search + sort + pagination.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[CheckPermission(StandardPermission.Orders.ORDERS_VIEW)]
|
||||||
|
public async Task<IActionResult> FruitBankOrderList()
|
||||||
|
{
|
||||||
|
// ── 1. Parse DataTables protocol params ────────────────────────
|
||||||
|
_ = 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 ? 50 : Math.Min(length, 500);
|
||||||
|
|
||||||
|
// Sort column
|
||||||
|
_ = 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() ?? "Id";
|
||||||
|
|
||||||
|
// Per-column search values keyed by column data-field name
|
||||||
|
var colSearch = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (int ci = 0; Request.Form.ContainsKey($"columns[{ci}][data]"); ci++)
|
||||||
|
{
|
||||||
|
var cData = Request.Form[$"columns[{ci}][data]"].FirstOrDefault();
|
||||||
|
var cVal = Request.Form[$"columns[{ci}][search][value]"].FirstOrDefault();
|
||||||
|
if (!string.IsNullOrWhiteSpace(cData) && !string.IsNullOrWhiteSpace(cVal))
|
||||||
|
colSearch[cData] = cVal.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Parse custom filter params ─────────────────────────────
|
||||||
|
DateTime? startDate = null, endDate = null;
|
||||||
|
if (DateTime.TryParse(Request.Form["StartDate"].FirstOrDefault(), out var sd)) startDate = sd;
|
||||||
|
if (DateTime.TryParse(Request.Form["EndDate"].FirstOrDefault(), out var ed)) endDate = ed;
|
||||||
|
|
||||||
|
var orderStatusIds = Request.Form["OrderStatusIds"]
|
||||||
|
.Where(v => int.TryParse(v, out _)).Select(int.Parse).Where(id => id > 0).ToList();
|
||||||
|
var paymentStatusIds = Request.Form["PaymentStatusIds"]
|
||||||
|
.Where(v => int.TryParse(v, out _)).Select(int.Parse).Where(id => id > 0).ToList();
|
||||||
|
var shippingStatusIds = Request.Form["ShippingStatusIds"]
|
||||||
|
.Where(v => int.TryParse(v, out _)).Select(int.Parse).Where(id => id > 0).ToList();
|
||||||
|
|
||||||
|
var billingCompany = Request.Form["BillingCompany"].FirstOrDefault(); // holds customer ID (string)
|
||||||
|
|
||||||
|
bool? isMeasurableFilter = null;
|
||||||
|
var imStr = Request.Form["IsMeasurable"].FirstOrDefault();
|
||||||
|
if (imStr == "true") isMeasurableFilter = true;
|
||||||
|
if (imStr == "false") isMeasurableFilter = false;
|
||||||
|
|
||||||
|
bool? hasInnvoiceFilter = null;
|
||||||
|
var hiStr = Request.Form["HasInnvoiceTechId"].FirstOrDefault();
|
||||||
|
if (hiStr == "true") hasInnvoiceFilter = true;
|
||||||
|
if (hiStr == "false") hasInnvoiceFilter = false;
|
||||||
|
|
||||||
|
// ── 3. Fetch data via factory (applies NopCommerce base filters)
|
||||||
|
// We ask for a large page so all matching records come back in one shot;
|
||||||
|
// FruitBank-specific filtering + pagination happen below in-process.
|
||||||
|
var searchModel = new OrderSearchModelExtended
|
||||||
|
{
|
||||||
|
StartDate = startDate,
|
||||||
|
EndDate = endDate,
|
||||||
|
OrderStatusIds = orderStatusIds.Any() ? orderStatusIds : null,
|
||||||
|
PaymentStatusIds = paymentStatusIds.Any() ? paymentStatusIds : null,
|
||||||
|
ShippingStatusIds = shippingStatusIds.Any() ? shippingStatusIds : null,
|
||||||
|
BillingCompany = billingCompany,
|
||||||
|
SortColumn = "Id",
|
||||||
|
SortColumnDirection = "desc"
|
||||||
|
};
|
||||||
|
// SetGridPageSize is the proper NopCommerce way to override Page/PageSize
|
||||||
|
searchModel.SetGridPageSize(5000, "5000");
|
||||||
|
|
||||||
|
OrderListModelExtended orderListModel;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
orderListModel = await _orderModelFactory.PrepareOrderListModelExtendedAsync(searchModel);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error($"FruitBankOrderList – factory error: {ex.Message}", ex);
|
||||||
|
return Json(new { draw, recordsTotal = 0, recordsFiltered = 0, data = Array.Empty<object>() });
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = orderListModel.Data?.ToList() ?? new List<OrderModelExtended>();
|
||||||
|
int total = orderListModel.RecordsTotal;
|
||||||
|
|
||||||
|
// ── 4. Map to lightweight DTO ──────────────────────────────────
|
||||||
|
var dtos = rows.Select(o => new Nop.Plugin.Misc.FruitBankPlugin.Models.Orders.FruitBankOrderRowDto
|
||||||
|
{
|
||||||
|
Id = o.Id,
|
||||||
|
CustomOrderNumber = o.CustomOrderNumber,
|
||||||
|
CustomerCompany = o.CustomerCompany,
|
||||||
|
CustomerId = o.CustomerId,
|
||||||
|
InnvoiceTechId = o.InnvoiceTechId,
|
||||||
|
IsAllOrderItemAvgWeightValid = o.IsAllOrderItemAvgWeightValid,
|
||||||
|
IsMeasurable = o.IsMeasurable,
|
||||||
|
MeasuringStatus = (int)o.MeasuringStatus,
|
||||||
|
MeasuringStatusString = o.MeasuringStatusString,
|
||||||
|
DateOfReceipt = o.DateOfReceipt,
|
||||||
|
OrderStatusId = o.OrderStatusId,
|
||||||
|
OrderStatus = o.OrderStatus,
|
||||||
|
PaymentStatusId = o.PaymentStatusId,
|
||||||
|
PaymentStatus = o.PaymentStatus,
|
||||||
|
ShippingStatusId = o.ShippingStatusId,
|
||||||
|
ShippingStatus = o.ShippingStatus,
|
||||||
|
StoreName = o.StoreName,
|
||||||
|
CreatedOn = o.CreatedOn,
|
||||||
|
OrderTotal = o.OrderTotal
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// ── 5. Apply FruitBank-specific top-level filters ──────────────
|
||||||
|
if (isMeasurableFilter.HasValue)
|
||||||
|
dtos = dtos.Where(o => o.IsMeasurable == isMeasurableFilter.Value).ToList();
|
||||||
|
|
||||||
|
if (hasInnvoiceFilter.HasValue)
|
||||||
|
dtos = hasInnvoiceFilter.Value
|
||||||
|
? dtos.Where(o => !string.IsNullOrEmpty(o.InnvoiceTechId)).ToList()
|
||||||
|
: dtos.Where(o => string.IsNullOrEmpty(o.InnvoiceTechId)).ToList();
|
||||||
|
|
||||||
|
// ── 6. Apply per-column search ─────────────────────────────────
|
||||||
|
foreach (var (col, val) in colSearch)
|
||||||
|
{
|
||||||
|
dtos = col.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"customordernumber" => dtos.Where(o => o.CustomOrderNumber?.Contains(val, StringComparison.OrdinalIgnoreCase) == true).ToList(),
|
||||||
|
"customercompany" => dtos.Where(o => o.CustomerCompany?.Contains(val, StringComparison.OrdinalIgnoreCase) == true).ToList(),
|
||||||
|
"orderstatusid" => int.TryParse(val, out int osId) ? dtos.Where(o => o.OrderStatusId == osId).ToList() : dtos,
|
||||||
|
"measuringstatus" => int.TryParse(val, out int msId) ? dtos.Where(o => o.MeasuringStatus == msId).ToList() : dtos,
|
||||||
|
"ismeasurable" => bool.TryParse(val, out bool bm) ? dtos.Where(o => o.IsMeasurable == bm).ToList() : dtos,
|
||||||
|
// InnVoice column sends 'has' or 'none' strings
|
||||||
|
"innvoicetechid" => val == "has" ? dtos.Where(o => !string.IsNullOrEmpty(o.InnvoiceTechId)).ToList()
|
||||||
|
: val == "none" ? dtos.Where(o => string.IsNullOrEmpty(o.InnvoiceTechId)).ToList()
|
||||||
|
: dtos,
|
||||||
|
_ => dtos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int recordsFiltered = dtos.Count;
|
||||||
|
|
||||||
|
// ── 7. Sort ────────────────────────────────────────────────────
|
||||||
|
bool asc = sortDir == "asc";
|
||||||
|
dtos = sortColName.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"id" => asc ? dtos.OrderBy(o => o.Id).ToList() : dtos.OrderByDescending(o => o.Id).ToList(),
|
||||||
|
"customordernumber" => asc ? dtos.OrderBy(o => o.CustomOrderNumber).ToList() : dtos.OrderByDescending(o => o.CustomOrderNumber).ToList(),
|
||||||
|
"customercompany" => asc ? dtos.OrderBy(o => o.CustomerCompany).ToList() : dtos.OrderByDescending(o => o.CustomerCompany).ToList(),
|
||||||
|
"dateofreceipt" => asc ? dtos.OrderBy(o => o.DateOfReceipt ?? DateTime.MinValue).ToList() : dtos.OrderByDescending(o => o.DateOfReceipt ?? DateTime.MinValue).ToList(),
|
||||||
|
"createdon" => asc ? dtos.OrderBy(o => o.CreatedOn).ToList() : dtos.OrderByDescending(o => o.CreatedOn).ToList(),
|
||||||
|
"orderstatusid" => asc ? dtos.OrderBy(o => o.OrderStatusId).ToList() : dtos.OrderByDescending(o => o.OrderStatusId).ToList(),
|
||||||
|
"measuringstatus" => asc ? dtos.OrderBy(o => o.MeasuringStatus).ToList() : dtos.OrderByDescending(o => o.MeasuringStatus).ToList(),
|
||||||
|
_ => dtos.OrderByDescending(o => o.Id).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 8. Paginate ────────────────────────────────────────────────
|
||||||
|
var page = dtos.Skip(start).Take(length).ToList();
|
||||||
|
|
||||||
|
return Json(new { draw, recordsTotal = total, recordsFiltered, data = page });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inline-edit save endpoint. Currently supports DateOfReceipt.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[CheckPermission(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)]
|
||||||
|
public async Task<IActionResult> UpdateOrderField(int orderId, string field, string value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var order = await _orderService.GetOrderByIdAsync(orderId);
|
||||||
|
if (order == null)
|
||||||
|
return Json(new { success = false, error = "Rendelés nem található" });
|
||||||
|
|
||||||
|
switch (field?.ToUpperInvariant())
|
||||||
|
{
|
||||||
|
case "DATEOFRECEIPT":
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
await _genericAttributeService.SaveAttributeAsync<DateTime?>(order, "DateOfReceipt", null);
|
||||||
|
return Json(new { success = true, displayValue = (string)null });
|
||||||
|
}
|
||||||
|
if (DateTime.TryParse(value, out var newDate))
|
||||||
|
{
|
||||||
|
await _genericAttributeService.SaveAttributeAsync(order, "DateOfReceipt", newDate);
|
||||||
|
return Json(new { success = true, displayValue = newDate.ToString("yyyy. MM. dd. HH:mm") });
|
||||||
|
}
|
||||||
|
return Json(new { success = false, error = "Érvénytelen dátum formátum" });
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Json(new { success = false, error = $"Ismeretlen mező: {field}" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error($"UpdateOrderField error – orderId={orderId} field={field}: {ex.Message}", ex);
|
||||||
|
return Json(new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
|
||||||
|
|
||||||
|
public class CustomerCreditListRow
|
||||||
|
{
|
||||||
|
public int CustomerId { get; set; }
|
||||||
|
public string CustomerEmail { get; set; } = string.Empty;
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
public bool HasCreditLimit { get; set; }
|
||||||
|
public decimal CreditLimit { get; set; }
|
||||||
|
public decimal OutstandingBalance { get; set; }
|
||||||
|
public decimal? RemainingCredit { get; set; }
|
||||||
|
public string? Comment { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
|
||||||
|
|
||||||
|
public class CustomerCreditModel
|
||||||
|
{
|
||||||
|
public int CustomerId { get; set; }
|
||||||
|
public string CustomerEmail { get; set; } = string.Empty;
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Credit record
|
||||||
|
public int CreditId { get; set; }
|
||||||
|
public decimal CreditLimit { get; set; }
|
||||||
|
public string? Comment { get; set; }
|
||||||
|
public bool HasCreditLimit { get; set; }
|
||||||
|
|
||||||
|
// Calculated
|
||||||
|
public decimal OutstandingBalance { get; set; }
|
||||||
|
public decimal? RemainingCredit { get; set; }
|
||||||
|
|
||||||
|
// Unpaid orders table
|
||||||
|
public List<CustomerCreditOrderRow> UnpaidOrders { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomerCreditOrderRow
|
||||||
|
{
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
public decimal OrderTotal { get; set; }
|
||||||
|
public DateTime CreatedOnUtc { get; set; }
|
||||||
|
public string OrderStatus { get; set; } = string.Empty;
|
||||||
|
public string PaymentStatus { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.CustomerCreditModel
|
||||||
|
@using Nop.Web.Framework.UI
|
||||||
|
|
||||||
|
@{
|
||||||
|
// Layout = "_FruitBankAdminLayout";
|
||||||
|
var remaining = Model.RemainingCredit;
|
||||||
|
var statusClass = !Model.HasCreditLimit ? "status-unlimited"
|
||||||
|
: remaining <= 0 ? "status-blocked"
|
||||||
|
: remaining < Model.CreditLimit * 0.2m ? "status-warning"
|
||||||
|
: "status-ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.credit-summary { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
|
||||||
|
.credit-card { flex: 1; min-width: 160px; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem 1.25rem; }
|
||||||
|
.credit-card .label { font-size: 0.78rem; color: #666; margin-bottom: .3rem; }
|
||||||
|
.credit-card .value { font-size: 1.4rem; font-weight: 700; }
|
||||||
|
.status-ok .value { color: #2d7a3a; }
|
||||||
|
.status-warning .value { color: #f4a236; }
|
||||||
|
.status-blocked .value { color: #c0392b; }
|
||||||
|
.status-unlimited .value { color: #555; }
|
||||||
|
.back-link { margin-bottom: 1rem; display: inline-block; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<a class="back-link" href="/Admin/Customer/Edit/@Model.CustomerId">
|
||||||
|
<i class="fa fa-arrow-left"></i> @T("Plugins.Misc.FruitBankPlugin.CustomerCredit.BackToCustomer")
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="content-header clearfix">
|
||||||
|
<h1 class="pull-left">
|
||||||
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle") — @Model.CustomerName (@Model.CustomerEmail)
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* ── Summary cards ── *@
|
||||||
|
<div class="credit-summary">
|
||||||
|
<div class="credit-card">
|
||||||
|
<div class="label">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit")</div>
|
||||||
|
<div class="value">@(Model.HasCreditLimit ? Model.CreditLimit.ToString("N0") + " Ft" : "—")</div>
|
||||||
|
</div>
|
||||||
|
<div class="credit-card">
|
||||||
|
<div class="label">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance")</div>
|
||||||
|
<div class="value">@Model.OutstandingBalance.ToString("N0") Ft</div>
|
||||||
|
</div>
|
||||||
|
<div class="credit-card @statusClass">
|
||||||
|
<div class="label">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit")</div>
|
||||||
|
<div class="value">
|
||||||
|
@if (!Model.HasCreditLimit)
|
||||||
|
{
|
||||||
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@remaining!.Value.ToString("N0")
|
||||||
|
<span style="font-size:.9rem">Ft</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* ── Edit form ── *@
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.EditTitle")</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form asp-action="Save" asp-controller="CustomerCredit" asp-area="Admin" method="post">
|
||||||
|
<input type="hidden" name="CustomerId" value="@Model.CustomerId" />
|
||||||
|
<input type="hidden" name="CreditId" value="@Model.CreditId" />
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3 col-form-label">
|
||||||
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit")</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<input type="number" name="CreditLimit" value="@Model.CreditLimit" min="0" step="1000" class="form-control" style="max-width:240px" />
|
||||||
|
<small class="form-text text-muted">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimitHint")</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3 col-form-label">
|
||||||
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Comment")</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<textarea name="Comment" class="form-control" rows="3" style="max-width:480px">@Model.Comment</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-9 offset-md-3">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fa fa-save"></i> @T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Save")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* ── Unpaid orders table ── *@
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.UnpaidOrdersTitle") (@Model.UnpaidOrders.Count)</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
@if (!Model.UnpaidOrders.Any())
|
||||||
|
{
|
||||||
|
<p class="text-muted">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.NoUnpaidOrders")</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table table-bordered table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderId")</th>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderDate")</th>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderTotal")</th>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderStatus")</th>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.PaymentStatus")</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var o in Model.UnpaidOrders)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>#@o.OrderId</td>
|
||||||
|
<td>@o.CreatedOnUtc.ToLocalTime().ToString("yyyy.MM.dd HH:mm")</td>
|
||||||
|
<td><strong>@o.OrderTotal.ToString("N0") Ft</strong></td>
|
||||||
|
<td>@o.OrderStatus</td>
|
||||||
|
<td>@o.PaymentStatus</td>
|
||||||
|
<td>
|
||||||
|
<a href="/Admin/Order/Edit/@o.OrderId" class="btn btn-xs btn-default" target="_blank">
|
||||||
|
<i class="fa fa-external-link"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><strong>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Total")</strong></td>
|
||||||
|
<td><strong>@Model.OutstandingBalance.ToString("N0") Ft</strong></td>
|
||||||
|
<td colspan="3"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
@{
|
||||||
|
ViewBag.PageTitle = "Hitelkeretek";
|
||||||
|
NopHtml.SetActiveMenuItemSystemName("CustomerCredit.List");
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="content-header clearfix">
|
||||||
|
<h1 class="float-left">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle")</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<table id="cc-grid" class="table table-bordered table-hover m-0" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.CustomerName")</th>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.CustomerEmail")</th>
|
||||||
|
<th title="Kattintásra szerkeszthető — törléshez hagyd üresen">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit") <small class="text-muted">✏️</small></th>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance")</th>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit")</th>
|
||||||
|
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Comment")</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ── Editable credit limit cell ─────────────────────────────── */
|
||||||
|
#cc-grid tbody td.cc-editable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#cc-grid tbody td.cc-editable:hover {
|
||||||
|
background-color: #fff8e1;
|
||||||
|
}
|
||||||
|
#cc-grid tbody td.cc-editable input[type="number"] {
|
||||||
|
width: 130px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 1px solid #80bdff;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
/* ── Status colours ─────────────────────────────────────────── */
|
||||||
|
.cc-remaining-ok { color: #2d7a3a; font-weight: 600; }
|
||||||
|
.cc-remaining-warning { color: #e67e22; font-weight: 600; }
|
||||||
|
.cc-remaining-blocked { color: #c0392b; font-weight: 600; }
|
||||||
|
.cc-remaining-none { color: #888; }
|
||||||
|
/* ── Stripe + hover ─────────────────────────────────────────── */
|
||||||
|
#cc-grid tbody tr:nth-child(even) { background-color: #f9f9f9; }
|
||||||
|
#cc-grid tbody tr:hover { background-color: #eaf2ff; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
|
||||||
|
var _token = $('input[name="__RequestVerificationToken"]').val();
|
||||||
|
|
||||||
|
function fmt(val) {
|
||||||
|
if (val == null) return '—';
|
||||||
|
return Number(val).toLocaleString('hu-HU') + ' Ft';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRemaining(row) {
|
||||||
|
if (!row.HasCreditLimit) return '<span class="cc-remaining-none">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited").Text</span>';
|
||||||
|
var r = row.RemainingCredit;
|
||||||
|
var cls = r <= 0 ? 'cc-remaining-blocked'
|
||||||
|
: r < row.CreditLimit * 0.2 ? 'cc-remaining-warning'
|
||||||
|
: 'cc-remaining-ok';
|
||||||
|
return '<span class="' + cls + '">' + fmt(r) + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCreditLimit(row) {
|
||||||
|
if (!row.HasCreditLimit) return '<span class="text-muted">@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited").Text</span>';
|
||||||
|
return fmt(row.CreditLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
var table = $('#cc-grid').DataTable({
|
||||||
|
serverSide : true,
|
||||||
|
processing : true,
|
||||||
|
orderCellsTop: true,
|
||||||
|
pageLength : 25,
|
||||||
|
lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]],
|
||||||
|
order : [[3, 'desc']],
|
||||||
|
language : {
|
||||||
|
processing : 'Betöltés...',
|
||||||
|
search : 'Keresés:',
|
||||||
|
lengthMenu : '_MENU_ sor/oldal',
|
||||||
|
info : '_START_ – _END_ / _TOTAL_ ügyfél',
|
||||||
|
infoEmpty : '0 ügyfél',
|
||||||
|
infoFiltered : '(szűrve _MAX_-ból)',
|
||||||
|
paginate : { first: '««', previous: '«', next: '»', last: '»»' },
|
||||||
|
emptyTable : 'Nincs találat',
|
||||||
|
zeroRecords : 'Nincs találat'
|
||||||
|
},
|
||||||
|
ajax: {
|
||||||
|
url : '/Admin/CustomerCredit/CustomerCreditList',
|
||||||
|
type: 'POST',
|
||||||
|
data: function (d) {
|
||||||
|
d.__RequestVerificationToken = _token;
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
console.error('CustomerCreditList error:', xhr.status, xhr.responseText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
/* 0 */ { data: 'CustomerName', name: 'CustomerName' },
|
||||||
|
/* 1 */ { data: 'CustomerEmail', name: 'CustomerEmail' },
|
||||||
|
/* 2 */ { data: 'CreditLimit', name: 'CreditLimit', className: 'cc-editable text-right',
|
||||||
|
render: function (d, t, row) { return renderCreditLimit(row); } },
|
||||||
|
/* 3 */ { data: 'OutstandingBalance', name: 'OutstandingBalance', className: 'text-right',
|
||||||
|
render: function (d) {
|
||||||
|
var color = d > 0 ? 'color:#c0392b;font-weight:600' : '';
|
||||||
|
return '<span style="' + color + '">' + fmt(d) + '</span>';
|
||||||
|
}},
|
||||||
|
/* 4 */ { data: 'RemainingCredit', name: 'RemainingCredit',
|
||||||
|
render: function (d, t, row) { return renderRemaining(row); } },
|
||||||
|
/* 5 */ { data: 'Comment', name: 'Comment', orderable: false,
|
||||||
|
render: function (d) { return d ? '<span class="text-muted">' + d + '</span>' : ''; } },
|
||||||
|
/* 6 */ { data: 'CustomerId', name: null, orderable: false, searchable: false, width: '80px', className: 'text-center',
|
||||||
|
render: function (d) {
|
||||||
|
return '<a href="/Admin/CustomerCredit/Details/' + d + '" class="btn btn-default btn-xs" title="Részletek"><i class="fas fa-edit"></i></a>' +
|
||||||
|
' <a href="/Admin/Customer/Edit/' + d + '" class="btn btn-default btn-xs" title="Ügyfél szerkesztése"><i class="fas fa-user"></i></a>';
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Inline editing: CreditLimit ─────────────────────────────── */
|
||||||
|
$(document).on('click', '#cc-grid tbody td.cc-editable', function () {
|
||||||
|
var $td = $(this);
|
||||||
|
if ($td.find('input').length) return;
|
||||||
|
|
||||||
|
var $row = $td.closest('tr');
|
||||||
|
var rowData = table.row($row).data();
|
||||||
|
if (!rowData) return;
|
||||||
|
|
||||||
|
var savedHtml = $td.html();
|
||||||
|
var current = rowData.HasCreditLimit ? rowData.CreditLimit : '';
|
||||||
|
|
||||||
|
var $inp = $('<input type="number" min="0" step="1000" placeholder="Korlátlan (törléshez hagyd üresen)">')
|
||||||
|
.val(current)
|
||||||
|
.css({ width: '180px', fontSize: '13px' });
|
||||||
|
|
||||||
|
$td.html('').append($inp);
|
||||||
|
$inp.focus().select();
|
||||||
|
|
||||||
|
function restore() { $td.html(savedHtml); }
|
||||||
|
|
||||||
|
function persist() {
|
||||||
|
var raw = $inp.val().trim();
|
||||||
|
var removeLimit = raw === ''; // empty = remove limit → unlimited
|
||||||
|
var newVal = removeLimit ? null : parseFloat(raw);
|
||||||
|
|
||||||
|
// If a number was typed but is invalid or negative, cancel
|
||||||
|
if (!removeLimit && (isNaN(newVal) || newVal < 0)) { restore(); return; }
|
||||||
|
|
||||||
|
// No change: still has limit and same value
|
||||||
|
if (!removeLimit && rowData.HasCreditLimit && newVal === rowData.CreditLimit) { restore(); return; }
|
||||||
|
|
||||||
|
// No change: was already unlimited and still wants unlimited
|
||||||
|
if (removeLimit && !rowData.HasCreditLimit) { restore(); return; }
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url : '/Admin/CustomerCredit/UpdateCreditLimit',
|
||||||
|
type : 'POST',
|
||||||
|
data : {
|
||||||
|
__RequestVerificationToken : _token,
|
||||||
|
customerId : rowData.CustomerId,
|
||||||
|
creditLimit : removeLimit ? '' : newVal, // empty string signals "remove"
|
||||||
|
removeLimit : removeLimit,
|
||||||
|
comment : rowData.Comment || ''
|
||||||
|
},
|
||||||
|
success: function (res) {
|
||||||
|
if (res.success) {
|
||||||
|
rowData.CreditLimit = res.creditLimit;
|
||||||
|
rowData.OutstandingBalance = res.outstanding;
|
||||||
|
rowData.RemainingCredit = res.remaining;
|
||||||
|
rowData.HasCreditLimit = res.hasLimit;
|
||||||
|
table.row($row).data(rowData).invalidate().draw(false);
|
||||||
|
} else {
|
||||||
|
restore();
|
||||||
|
alert('Mentési hiba: ' + (res.error || 'Ismeretlen hiba'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () { restore(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$inp.on('blur', function () { persist(); });
|
||||||
|
$inp.on('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter') { $inp.off('blur'); persist(); }
|
||||||
|
if (e.key === 'Escape') { $inp.off('blur'); restore(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,634 @@
|
||||||
|
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.Order.OrderSearchModelExtended
|
||||||
|
|
||||||
|
@using FruitBank.Common.Interfaces
|
||||||
|
@using Nop.Services.Stores
|
||||||
|
@using Nop.Web.Areas.Admin.Components
|
||||||
|
@using Nop.Web.Areas.Admin.Models.Orders
|
||||||
|
@using Nop.Web.Framework.Infrastructure
|
||||||
|
@inject IStoreService storeService
|
||||||
|
|
||||||
|
@{
|
||||||
|
// Layout = "~/Areas/Admin/Views/Shared/_LayoutAdmin.cshtml";
|
||||||
|
ViewBag.PageTitle = "FruitBank Rendelések";
|
||||||
|
NopHtml.SetActiveMenuItemSystemName("Orders");
|
||||||
|
}
|
||||||
|
|
||||||
|
@* ── Action buttons ─────────────────────────────────────────────── *@
|
||||||
|
<form id="fb-header-form" asp-controller="Order" asp-action="List" method="post">
|
||||||
|
<div class="content-header clearfix">
|
||||||
|
<h1 class="float-left">Rendelések</h1>
|
||||||
|
<div class="float-right">
|
||||||
|
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#create-order-window">
|
||||||
|
<i class="fas fa-plus"></i> @T("Admin.Common.AddNew")
|
||||||
|
</button>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-success"><i class="fas fa-download"></i> @T("Admin.Common.Export")</button>
|
||||||
|
<button type="button" class="btn btn-success dropdown-toggle dropdown-icon" data-toggle="dropdown"><span class="sr-only"> </span></button>
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<li class="dropdown-item"><button asp-action="ExportXml" type="submit" name="exportxml-all"><i class="far fa-file-code"></i> @T("Admin.Common.ExportToXml.All")</button></li>
|
||||||
|
<li class="dropdown-item"><button type="button" id="exportxml-selected"><i class="far fa-file-code"></i> @T("Admin.Common.ExportToXml.Selected")</button></li>
|
||||||
|
<li class="dropdown-divider"></li>
|
||||||
|
<li class="dropdown-item"><button asp-action="ExportExcel" type="submit" name="exportexcel-all"><i class="far fa-file-excel"></i> @T("Admin.Common.ExportToExcel.All")</button></li>
|
||||||
|
<li class="dropdown-item"><button type="button" id="exportexcel-selected"><i class="far fa-file-excel"></i> @T("Admin.Common.ExportToExcel.Selected")</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-info"><i class="far fa-file-pdf"></i> @T("Admin.Orders.PdfInvoices")</button>
|
||||||
|
<button type="button" class="btn btn-info dropdown-toggle dropdown-icon" data-toggle="dropdown"><span class="sr-only"> </span></button>
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<li class="dropdown-item"><button asp-action="PdfInvoice" type="submit" name="pdf-invoice-all">@T("Admin.Orders.PdfInvoices.All")</button></li>
|
||||||
|
<li class="dropdown-item"><button type="button" id="pdf-invoice-selected">@T("Admin.Orders.PdfInvoices.Selected")</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
@* ── Filter Panel ─────────────────────────────────────────────── *@
|
||||||
|
<div class="card card-default card-search mb-2">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<div class="row align-items-end">
|
||||||
|
@* Date from *@
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<nop-label asp-for="StartDate" />
|
||||||
|
<nop-editor asp-for="StartDate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@* Date to *@
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<nop-label asp-for="EndDate" />
|
||||||
|
<nop-editor asp-for="EndDate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@* Partner autocomplete → stores customer ID in hidden *@
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label class="col-form-label">Partner</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="fb-company-display" autocomplete="off" class="form-control" placeholder="Cég neve..." />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" id="fb-company-clear" class="btn btn-outline-secondary" style="display:none">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input asp-for="BillingCompany" type="hidden" id="BillingCompany" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@* Go to order by number *@
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<nop-label asp-for="GoDirectlyToCustomOrderNumber" />
|
||||||
|
<div class="input-group">
|
||||||
|
<nop-editor asp-for="GoDirectlyToCustomOrderNumber" />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" id="go-to-order-by-number" class="btn btn-info">
|
||||||
|
@T("Admin.Common.Go")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@* Search button *@
|
||||||
|
<div class="col-md-1">
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<label class="col-form-label"> </label>
|
||||||
|
<button type="button" id="fb-search-btn" class="btn btn-primary btn-block">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* ── Grid ─────────────────────────────────────────────────────── *@
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
@* Anti-forgery token for AJAX POSTs *@
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<table id="fb-orders-grid" class="table table-bordered table-hover m-0" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><input type="checkbox" id="fb-check-all" title="Összes kijelölése"></th>
|
||||||
|
<th>Rendelés #</th>
|
||||||
|
<th>Partner</th>
|
||||||
|
<th>InnVoice</th>
|
||||||
|
<th>Súly</th>
|
||||||
|
<th>Mérhető</th>
|
||||||
|
<th>Mérés</th>
|
||||||
|
<th title="Kattintásra szerkeszthető">Átvétel <small class="text-muted">✏️</small></th>
|
||||||
|
<th>Státusz</th>
|
||||||
|
<th>Fizetés</th>
|
||||||
|
<th>Szállítás</th>
|
||||||
|
<th>Létrehozva</th>
|
||||||
|
<th>Összeg</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="fb-totals-row" class="card-footer py-2" style="display:none">
|
||||||
|
<div id="fb-totals-content" class="small text-muted"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
@* ── Export selected – XML ──────────────────────────────────────── *@
|
||||||
|
<form asp-controller="Order" asp-action="ExportXmlSelected" method="post" id="export-xml-selected-form">
|
||||||
|
<input type="hidden" id="export-xml-ids" name="selectedIds" value="" />
|
||||||
|
</form>
|
||||||
|
@* ── Export selected – Excel ────────────────────────────────────── *@
|
||||||
|
<form asp-controller="Order" asp-action="ExportExcelSelected" method="post" id="export-excel-selected-form">
|
||||||
|
<input type="hidden" id="export-excel-ids" name="selectedIds" value="" />
|
||||||
|
</form>
|
||||||
|
@* ── PDF selected ───────────────────────────────────────────────── *@
|
||||||
|
<form asp-controller="Order" asp-action="PdfInvoiceSelected" method="post" id="pdf-invoice-selected-form">
|
||||||
|
<input type="hidden" id="pdf-invoice-ids" name="selectedIds" value="" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@* ── Create Order Modal ─────────────────────────────────────────── *@
|
||||||
|
<div id="create-order-window" class="modal fade" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">@T("Admin.Orders.AddNew")</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||||
|
</div>
|
||||||
|
<form asp-controller="CustomOrder" asp-action="Create" method="post" id="create-order-form">
|
||||||
|
<div class="form-horizontal">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3"><label class="col-form-label">@T("Admin.Orders.Fields.Customer")</label></div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<input type="text" id="create-order-customer-search" autocomplete="off" class="form-control" placeholder="Ügyfél neve vagy email..." />
|
||||||
|
<span id="create-order-customer-name" class="mt-1 d-block"></span>
|
||||||
|
<input type="hidden" id="create-order-customer-id" name="customerId" />
|
||||||
|
<span class="field-validation-error" id="create-order-customer-error" style="display:none">Kérjük válasszon ügyfelet</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row" id="create-product-search-section" style="display:none">
|
||||||
|
<div class="col-md-3"><label class="col-form-label">@T("Admin.Orders.Fields.Product")</label></div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<input type="text" id="create-order-product-search" autocomplete="off" class="form-control" placeholder="Termék neve vagy SKU..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="create-selected-products-section" style="display:none">
|
||||||
|
<table class="table table-sm table-bordered" id="create-products-table">
|
||||||
|
<thead><tr><th>Termék</th><th style="width:100px">Menny.</th><th style="width:120px">Egységár</th><th style="width:40px"></th></tr></thead>
|
||||||
|
<tbody id="create-products-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="create-order-products-json" name="orderProductsJson" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">@T("Admin.Common.Cancel")</button>
|
||||||
|
<button type="submit" class="btn btn-primary">@T("Admin.Common.Create")</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ── Column filter row ──────────────────────────────────────── */
|
||||||
|
#fb-orders-grid thead tr.fb-filter-row th {
|
||||||
|
padding: 3px 4px;
|
||||||
|
background-color: #f4f6f9;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
#fb-orders-grid thead tr.fb-filter-row input,
|
||||||
|
#fb-orders-grid thead tr.fb-filter-row select {
|
||||||
|
width: 100%;
|
||||||
|
height: 26px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
#fb-orders-grid thead tr.fb-filter-row input:focus,
|
||||||
|
#fb-orders-grid thead tr.fb-filter-row select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #80bdff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0,123,255,.15);
|
||||||
|
}
|
||||||
|
/* ── Editable date cell ────────────────────────────────────── */
|
||||||
|
#fb-orders-grid tbody td.fb-editable-date {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#fb-orders-grid tbody td.fb-editable-date:hover {
|
||||||
|
background-color: #fff8e1;
|
||||||
|
}
|
||||||
|
#fb-orders-grid tbody td.fb-editable-date input[type="date"] {
|
||||||
|
width: 120px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border: 1px solid #80bdff;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
/* ── Stripe rows ───────────────────────────────────────────── */
|
||||||
|
#fb-orders-grid tbody tr:nth-child(even) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
#fb-orders-grid tbody tr:hover {
|
||||||
|
background-color: #eaf2ff;
|
||||||
|
}
|
||||||
|
/* ── Processing overlay ────────────────────────────────────── */
|
||||||
|
#fb-orders-grid_processing {
|
||||||
|
background: rgba(255,255,255,0.85);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
/* autocomplete z-index fix in modals */
|
||||||
|
.ui-autocomplete { z-index: 1060 !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
|
||||||
|
/* ── Helpers ─────────────────────────────────────────────────── */
|
||||||
|
var _token = $('input[name="__RequestVerificationToken"]').val();
|
||||||
|
|
||||||
|
function antiForgery(obj) {
|
||||||
|
obj['__RequestVerificationToken'] = _token;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedIds = [];
|
||||||
|
|
||||||
|
function getSelectedIds() {
|
||||||
|
selectedIds = [];
|
||||||
|
$('#fb-orders-grid tbody .fb-row-check:checked').each(function () {
|
||||||
|
selectedIds.push($(this).val());
|
||||||
|
});
|
||||||
|
return selectedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Column renderers ────────────────────────────────────────── */
|
||||||
|
function renderInnvoice(data) {
|
||||||
|
return data
|
||||||
|
? '<span class="badge badge-success">Igen</span>'
|
||||||
|
: '<span class="badge badge-secondary">Nem</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWeightValid(data) {
|
||||||
|
return data
|
||||||
|
? '<span class="badge badge-success">OK</span>'
|
||||||
|
: '<span class="badge badge-danger font-weight-bold">!</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMeasurable(data) {
|
||||||
|
return data
|
||||||
|
? '<span class="badge badge-info">Igen</span>'
|
||||||
|
: '<span class="badge badge-light text-secondary">Nem</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMeasuringStatus(val, row) {
|
||||||
|
var map = { 10: 'warning', 20: 'primary', 30: 'success', 40: 'danger' };
|
||||||
|
var cls = map[val] || 'secondary';
|
||||||
|
var label = row.MeasuringStatusString || String(val);
|
||||||
|
return '<span class="badge badge-' + cls + '">' + label + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDateOfReceipt(data) {
|
||||||
|
if (!data) return '<span class="text-muted">—</span>';
|
||||||
|
var d = new Date(data);
|
||||||
|
var dateStr = d.toLocaleDateString('hu-HU');
|
||||||
|
var timeStr = d.toLocaleTimeString('hu-HU', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
return '<span>' + dateStr + ' ' + timeStr + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOrderStatus(statusId, row) {
|
||||||
|
var map = { 10: 'warning', 20: 'primary', 30: 'success', 40: 'danger' };
|
||||||
|
var cls = map[statusId] || 'secondary';
|
||||||
|
var label = row.OrderStatus || String(statusId);
|
||||||
|
return '<span class="badge badge-' + cls + '">' + label + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── DataTables ──────────────────────────────────────────────── */
|
||||||
|
var table = $('#fb-orders-grid').DataTable({
|
||||||
|
serverSide : true,
|
||||||
|
processing : true,
|
||||||
|
orderCellsTop: true,
|
||||||
|
stateSave : false,
|
||||||
|
pageLength : 50,
|
||||||
|
lengthMenu : [[20, 50, 100, 200, 500], [20, 50, 100, 200, 500]],
|
||||||
|
order : [[0, 'desc']],
|
||||||
|
language : {
|
||||||
|
processing : 'Betöltés...',
|
||||||
|
search : 'Keresés:',
|
||||||
|
lengthMenu : '_MENU_ sor/oldal',
|
||||||
|
info : '_START_ – _END_ / _TOTAL_ rendelés',
|
||||||
|
infoEmpty : '0 rendelés',
|
||||||
|
infoFiltered : '(szűrve _MAX_-ból)',
|
||||||
|
paginate : { first: '««', previous: '«', next: '»', last: '»»' },
|
||||||
|
emptyTable : 'Nincs találat',
|
||||||
|
zeroRecords : 'Nincs találat a szűrési feltételekre'
|
||||||
|
},
|
||||||
|
ajax: {
|
||||||
|
url : '@Url.Action("FruitBankOrderList", "CustomOrder")',
|
||||||
|
type: 'POST',
|
||||||
|
data: function (d) {
|
||||||
|
d.StartDate = $('#@Html.IdFor(m => m.StartDate)').val();
|
||||||
|
d.EndDate = $('#@Html.IdFor(m => m.EndDate)').val();
|
||||||
|
d.BillingCompany = $('#BillingCompany').val();
|
||||||
|
addAntiForgeryToken(d);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
console.error('FruitBankOrderList AJAX error:', xhr.status, xhr.responseText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
/* 0 */ { data: 'Id', name: 'Id', orderable: false, searchable: false, width: '32px',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function (d) { return '<input type="checkbox" class="fb-row-check" value="' + d + '">'; } },
|
||||||
|
/* 1 */ { data: 'CustomOrderNumber', name: 'CustomOrderNumber', width: '95px' },
|
||||||
|
/* 2 */ { data: 'CustomerCompany', name: 'CustomerCompany' },
|
||||||
|
/* 3 */ { data: 'InnvoiceTechId', name: 'InnvoiceTechId', orderable: false, width: '75px',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function (d) { return renderInnvoice(d); } },
|
||||||
|
/* 4 */ { data: 'IsAllOrderItemAvgWeightValid', name: 'IsAllOrderItemAvgWeightValid', orderable: false, width: '55px',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function (d) { return renderWeightValid(d); } },
|
||||||
|
/* 5 */ { data: 'IsMeasurable', name: 'IsMeasurable', orderable: false, width: '65px',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function (d) { return renderMeasurable(d); } },
|
||||||
|
/* 6 */ { data: 'MeasuringStatus', name: 'MeasuringStatus', width: '95px',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function (d, t, row) { return renderMeasuringStatus(d, row); } },
|
||||||
|
/* 7 */ { data: 'DateOfReceipt', name: 'DateOfReceipt', width: '110px',
|
||||||
|
className: 'text-center fb-editable-date',
|
||||||
|
render: function (d) { return renderDateOfReceipt(d); } },
|
||||||
|
/* 8 */ { data: 'OrderStatusId', name: 'OrderStatusId', width: '105px',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function (d, t, row) { return renderOrderStatus(d, row); } },
|
||||||
|
/* 9 */ { data: 'PaymentStatus', name: 'PaymentStatus', orderable: false, width: '110px',
|
||||||
|
render: function (d) { return d || '—'; } },
|
||||||
|
/* 10 */ { data: 'ShippingStatus', name: 'ShippingStatus', orderable: false, width: '110px',
|
||||||
|
render: function (d) { return d || '—'; } },
|
||||||
|
/* 11 */ { data: 'CreatedOn', name: 'CreatedOn', width: '92px',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function (d) { return d ? new Date(d).toLocaleDateString('hu-HU') : '—'; } },
|
||||||
|
/* 12 */ { data: 'OrderTotal', name: 'OrderTotal', orderable: false, width: '105px',
|
||||||
|
className: 'text-right' },
|
||||||
|
/* 13 */ { data: 'Id', name: null, orderable: false, searchable: false, width: '42px',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function (d) { return '<a href="/Admin/Order/Edit/' + d + '" class="btn btn-default btn-xs" title="Szerkesztés"><i class="fas fa-pencil-alt"></i></a>'; } }
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ── Per-column filter row ─────────────────────────────────── */
|
||||||
|
initComplete: function () {
|
||||||
|
var api = this.api();
|
||||||
|
var $thead = $(this).find('thead');
|
||||||
|
var $filterRow = $('<tr class="fb-filter-row"></tr>').appendTo($thead);
|
||||||
|
|
||||||
|
// Filter definition per column index:
|
||||||
|
// null = no filter, 'text' = text input, {type:'select', opts:[...]} = dropdown
|
||||||
|
var defs = [
|
||||||
|
null, /* 0 checkbox */
|
||||||
|
'text', /* 1 order # */
|
||||||
|
'text', /* 2 company */
|
||||||
|
{ type: 'select', opts: [['', 'Mind'], ['has', '✓ Igen'], ['none', '✗ Nem']] }, /* 3 innvoice */
|
||||||
|
null, /* 4 weight (no per-column filter) */
|
||||||
|
{ type: 'select', opts: [['', 'Mind'], ['true', 'Igen'], ['false', 'Nem']] }, /* 5 measurable */
|
||||||
|
{ type: 'select', opts: [['', 'Mind'], ['0', 'Nincs'], ['10', '…folyamat'], ['20', 'Mérésre'], ['30', 'Mérve'], ['40', 'Lezárva']] }, /* 6 measuring */
|
||||||
|
null, /* 7 date (top-level filter handles this) */
|
||||||
|
{ type: 'select', opts: [['', 'Mind'], ['10', 'Függőben'], ['20', 'Feldolgozás'], ['30', 'Teljesítve'], ['40', 'Törölve']] }, /* 8 order status */
|
||||||
|
null, /* 9 payment */
|
||||||
|
null, /* 10 shipping */
|
||||||
|
null, /* 11 created */
|
||||||
|
null, /* 12 total */
|
||||||
|
null /* 13 button */
|
||||||
|
];
|
||||||
|
|
||||||
|
api.columns().every(function (idx) {
|
||||||
|
var col = this;
|
||||||
|
var $th = $('<th></th>').appendTo($filterRow);
|
||||||
|
var def = defs[idx];
|
||||||
|
if (!def) return;
|
||||||
|
|
||||||
|
if (def === 'text') {
|
||||||
|
var $inp = $('<input type="text" placeholder="🔍">');
|
||||||
|
$inp.appendTo($th);
|
||||||
|
var timer;
|
||||||
|
$inp.on('input', function () {
|
||||||
|
clearTimeout(timer);
|
||||||
|
var v = this.value;
|
||||||
|
timer = setTimeout(function () { col.search(v).draw(); }, 450);
|
||||||
|
});
|
||||||
|
} else if (def.type === 'select') {
|
||||||
|
var $sel = $('<select></select>');
|
||||||
|
def.opts.forEach(function (o) {
|
||||||
|
$sel.append($('<option>').val(o[0]).text(o[1]));
|
||||||
|
});
|
||||||
|
$sel.appendTo($th);
|
||||||
|
$sel.on('change', function () { col.search(this.value).draw(); });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Search / filter triggers ────────────────────────────────── */
|
||||||
|
$('#fb-search-btn').on('click', function () { table.draw(); });
|
||||||
|
|
||||||
|
/* redraw on date change */
|
||||||
|
$('#@Html.IdFor(m => m.StartDate), #@Html.IdFor(m => m.EndDate)').on('change', function () { table.draw(); });
|
||||||
|
|
||||||
|
/* ── Partner (company) autocomplete ──────────────────────────── */
|
||||||
|
$('#fb-company-display').autocomplete({
|
||||||
|
delay : 400,
|
||||||
|
minLength: 2,
|
||||||
|
source : '@Url.Action("CustomerSearchAutoComplete", "CustomOrder")',
|
||||||
|
select : function (e, ui) {
|
||||||
|
$('#BillingCompany').val(ui.item.value);
|
||||||
|
$('#fb-company-display').val(ui.item.label);
|
||||||
|
$('#fb-company-clear').show();
|
||||||
|
table.draw();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#fb-company-clear').on('click', function () {
|
||||||
|
$('#BillingCompany').val('');
|
||||||
|
$('#fb-company-display').val('');
|
||||||
|
$(this).hide();
|
||||||
|
table.draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Checkbox: select all on current page ────────────────────── */
|
||||||
|
$('#fb-check-all').on('change', function () {
|
||||||
|
var checked = this.checked;
|
||||||
|
$('#fb-orders-grid tbody .fb-row-check').prop('checked', checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Inline editing: DateOfReceipt ───────────────────────────── */
|
||||||
|
$(document).on('click', '#fb-orders-grid tbody td.fb-editable-date', function (e) {
|
||||||
|
var $td = $(this);
|
||||||
|
if ($td.find('input').length) return; // already in edit mode
|
||||||
|
|
||||||
|
var $row = $td.closest('tr');
|
||||||
|
var rowData = table.row($row).data();
|
||||||
|
if (!rowData) return;
|
||||||
|
|
||||||
|
// Build a datetime-local string (YYYY-MM-DDTHH:mm) for the input value
|
||||||
|
function toDatetimeLocal(iso) {
|
||||||
|
if (!iso) return '';
|
||||||
|
var d = new Date(iso);
|
||||||
|
if (isNaN(d)) return '';
|
||||||
|
var pad = function(n) { return String(n).padStart(2, '0'); };
|
||||||
|
return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) +
|
||||||
|
'T' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentIso = rowData.DateOfReceipt || null;
|
||||||
|
// Default to now if no date set yet
|
||||||
|
var inputVal = currentIso ? toDatetimeLocal(currentIso) : toDatetimeLocal(new Date().toISOString());
|
||||||
|
var orderId = rowData.Id;
|
||||||
|
var savedHtml = $td.html();
|
||||||
|
|
||||||
|
var $inp = $('<input type="datetime-local">').val(inputVal);
|
||||||
|
$td.html('').append($inp);
|
||||||
|
$inp.focus();
|
||||||
|
|
||||||
|
function restore() { $td.html(savedHtml); }
|
||||||
|
|
||||||
|
function persist() {
|
||||||
|
var newVal = $inp.val(); // format: YYYY-MM-DDTHH:mm
|
||||||
|
// Compare against original ISO; skip save only if identical
|
||||||
|
var newIso = newVal ? new Date(newVal).toISOString() : null;
|
||||||
|
var oldIso = currentIso ? new Date(currentIso).toISOString() : null;
|
||||||
|
if (newIso === oldIso) { restore(); return; }
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url : '@Url.Action("UpdateOrderField", "CustomOrder")',
|
||||||
|
type : 'POST',
|
||||||
|
data : antiForgery({ orderId: orderId, field: 'DateOfReceipt', value: newVal }),
|
||||||
|
success: function (res) {
|
||||||
|
if (res.success) {
|
||||||
|
rowData.DateOfReceipt = newVal || null;
|
||||||
|
table.row($row).data(rowData).invalidate();
|
||||||
|
/* re-render only the date cell without full redraw */
|
||||||
|
$td.html(renderDateOfReceipt(newVal));
|
||||||
|
} else {
|
||||||
|
restore();
|
||||||
|
alert('Mentési hiba: ' + (res.error || 'Ismeretlen hiba'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () { restore(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$inp.on('blur', function () { persist(); });
|
||||||
|
$inp.on('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter') { $inp.off('blur'); persist(); }
|
||||||
|
if (e.key === 'Escape') { $inp.off('blur'); restore(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Go-to order by number ───────────────────────────────────── */
|
||||||
|
$('#@Html.IdFor(m => m.GoDirectlyToCustomOrderNumber)').on('keydown', function (e) {
|
||||||
|
if (e.keyCode === 13) { $('#go-to-order-by-number').trigger('click'); return false; }
|
||||||
|
});
|
||||||
|
$('#go-to-order-by-number').on('click', function () {
|
||||||
|
var num = $('#@Html.IdFor(m => m.GoDirectlyToCustomOrderNumber)').val();
|
||||||
|
if (num) {
|
||||||
|
window.location.href = '@Url.Action("GoToOrderId", "CustomOrder")?' +
|
||||||
|
'@Html.IdFor(m => m.GoDirectlyToCustomOrderNumber)=' + encodeURIComponent(num);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Export / PDF selected ───────────────────────────────────── */
|
||||||
|
function exportSelected(formId, inputId) {
|
||||||
|
var ids = getSelectedIds().join(',');
|
||||||
|
if (!ids) { alert('@T("Admin.Orders.NoOrders")'); return; }
|
||||||
|
$(inputId).val(ids);
|
||||||
|
$(formId).submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#exportxml-selected').on('click', function (e) { e.preventDefault(); exportSelected('#export-xml-selected-form', '#export-xml-ids'); });
|
||||||
|
$('#exportexcel-selected').on('click', function (e) { e.preventDefault(); exportSelected('#export-excel-selected-form', '#export-excel-ids'); });
|
||||||
|
$('#pdf-invoice-selected').on('click', function (e) { e.preventDefault(); exportSelected('#pdf-invoice-selected-form', '#pdf-invoice-ids'); });
|
||||||
|
|
||||||
|
/* ── Create order modal ──────────────────────────────────────── */
|
||||||
|
var createProducts = [];
|
||||||
|
|
||||||
|
$('#create-order-customer-search').autocomplete({
|
||||||
|
delay : 400,
|
||||||
|
minLength: 2,
|
||||||
|
source : '@Url.Action("CustomerSearchAutoComplete", "CustomOrder")',
|
||||||
|
select : function (e, ui) {
|
||||||
|
$('#create-order-customer-id').val(ui.item.value);
|
||||||
|
$('#create-order-customer-name').html('<strong>' + ui.item.label + '</strong>');
|
||||||
|
$('#create-order-customer-search').val('');
|
||||||
|
$('#create-product-search-section').slideDown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#create-order-product-search').autocomplete({
|
||||||
|
delay : 400,
|
||||||
|
minLength: 2,
|
||||||
|
source : '@Url.Action("ProductSearchAutoComplete", "CustomOrder")',
|
||||||
|
select : function (e, ui) {
|
||||||
|
addCreateProduct(ui.item);
|
||||||
|
$('#create-order-product-search').val('');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function addCreateProduct(item) {
|
||||||
|
if (createProducts.find(function (p) { return p.id === item.value; })) return;
|
||||||
|
createProducts.push({ id: item.value, name: item.label, quantity: 1, price: item.price || 0 });
|
||||||
|
renderCreateProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCreateProducts() {
|
||||||
|
var $body = $('#create-products-body').empty();
|
||||||
|
if (!createProducts.length) { $('#create-selected-products-section').hide(); return; }
|
||||||
|
$('#create-selected-products-section').show();
|
||||||
|
createProducts.forEach(function (p, i) {
|
||||||
|
$body.append(
|
||||||
|
'<tr>' +
|
||||||
|
'<td><strong>' + p.name + '</strong></td>' +
|
||||||
|
'<td><input type="number" class="form-control form-control-sm" min="1" value="' + p.quantity + '" data-idx="' + i + '" onchange="window._fbUpdateQty(this)"></td>' +
|
||||||
|
'<td><input type="text" class="form-control form-control-sm" value="' + p.price + '" data-idx="' + i + '" onchange="window._fbUpdatePrice(this)"></td>' +
|
||||||
|
'<td class="text-center"><button type="button" class="btn btn-danger btn-xs" onclick="window._fbRemoveProduct(' + i + ')"><i class="fas fa-trash"></i></button></td>' +
|
||||||
|
'</tr>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
$('#create-order-products-json').val(JSON.stringify(createProducts));
|
||||||
|
}
|
||||||
|
|
||||||
|
window._fbUpdateQty = function (el) { createProducts[+el.dataset.idx].quantity = +el.value; renderCreateProducts(); };
|
||||||
|
window._fbUpdatePrice = function (el) { createProducts[+el.dataset.idx].price = +el.value; renderCreateProducts(); };
|
||||||
|
window._fbRemoveProduct = function (i) { createProducts.splice(i, 1); renderCreateProducts(); };
|
||||||
|
|
||||||
|
$('#create-order-form').on('submit', function (e) {
|
||||||
|
if (!$('#create-order-customer-id').val()) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#create-order-customer-error').show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#create-order-window').on('hidden.bs.modal', function () {
|
||||||
|
$('#create-order-customer-search, #create-order-customer-id, #create-order-customer-name').val('').html('');
|
||||||
|
$('#create-order-customer-error').hide();
|
||||||
|
$('#create-product-search-section').hide();
|
||||||
|
createProducts = [];
|
||||||
|
renderCreateProducts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Models;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||||
|
using Nop.Web.Areas.Admin.Models.Customers;
|
||||||
|
using Nop.Web.Framework.Components;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Components;
|
||||||
|
|
||||||
|
[ViewComponent(Name = "CustomerCreditWidget")]
|
||||||
|
public class CustomerCreditWidgetViewComponent : NopViewComponent
|
||||||
|
{
|
||||||
|
private readonly ICustomerCreditService _customerCreditService;
|
||||||
|
|
||||||
|
public CustomerCreditWidgetViewComponent(ICustomerCreditService customerCreditService)
|
||||||
|
{
|
||||||
|
_customerCreditService = customerCreditService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IViewComponentResult> InvokeAsync(string widgetZone, object additionalData)
|
||||||
|
{
|
||||||
|
if (additionalData is not CustomerModel customerModel) return Content("");
|
||||||
|
|
||||||
|
var customerId = customerModel.Id;
|
||||||
|
var credit = await _customerCreditService.GetByCustomerIdAsync(customerId);
|
||||||
|
var outstanding = await _customerCreditService.GetOutstandingBalanceAsync(customerId);
|
||||||
|
|
||||||
|
var model = new CustomerCreditWidgetModel
|
||||||
|
{
|
||||||
|
CustomerId = customerId,
|
||||||
|
HasCreditLimit = credit != null,
|
||||||
|
CreditLimit = credit?.CreditLimit ?? 0m,
|
||||||
|
OutstandingBalance = outstanding,
|
||||||
|
RemainingCredit = credit != null ? credit.CreditLimit - outstanding : (decimal?)null,
|
||||||
|
Comment = credit?.Comment
|
||||||
|
};
|
||||||
|
|
||||||
|
return View("~/Plugins/Misc.FruitBankPlugin/Views/CustomerCreditWidget.cshtml", model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
|
using LinqToDB;
|
||||||
|
using Mango.Nop.Core.Loggers;
|
||||||
|
using Mango.Nop.Data.Repositories;
|
||||||
|
using Nop.Core.Caching;
|
||||||
|
using Nop.Core.Configuration;
|
||||||
|
using Nop.Core.Events;
|
||||||
|
using Nop.Data;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||||
|
|
||||||
|
public class CustomerCreditDbTable : MgDbTableBase<CustomerCredit>
|
||||||
|
{
|
||||||
|
public CustomerCreditDbTable(
|
||||||
|
IEventPublisher eventPublisher,
|
||||||
|
INopDataProvider dataProvider,
|
||||||
|
IShortTermCacheManager shortTermCacheManager,
|
||||||
|
IStaticCacheManager staticCacheManager,
|
||||||
|
AppSettings appSettings)
|
||||||
|
: base(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CustomerCredit?> GetByCustomerIdAsync(int customerId)
|
||||||
|
=> GetAll().FirstOrDefaultAsync(x => x.CustomerId == customerId);
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,8 @@ public class FruitBankDbContext : MgDbContextBase,
|
||||||
IShippingItemPalletDbSet<ShippingItemPalletDbTable>,
|
IShippingItemPalletDbSet<ShippingItemPalletDbTable>,
|
||||||
IOrderItemPalletDbSet<OrderItemPalletDbTable>,
|
IOrderItemPalletDbSet<OrderItemPalletDbTable>,
|
||||||
IShippingDocumentToFilesDbSet<ShippingDocumentToFilesDbTable>,
|
IShippingDocumentToFilesDbSet<ShippingDocumentToFilesDbTable>,
|
||||||
IFilesDbSet<FilesDbTable>
|
IFilesDbSet<FilesDbTable>,
|
||||||
|
ICustomerCreditDbSet<CustomerCreditDbTable>
|
||||||
{
|
{
|
||||||
private readonly FruitBankAttributeService _fruitBankAttributeService;
|
private readonly FruitBankAttributeService _fruitBankAttributeService;
|
||||||
private readonly IStoreContext _storeContext;
|
private readonly IStoreContext _storeContext;
|
||||||
|
|
@ -64,6 +65,7 @@ public class FruitBankDbContext : MgDbContextBase,
|
||||||
public FilesDbTable Files { get; set; }
|
public FilesDbTable Files { get; set; }
|
||||||
public ShippingDocumentToFilesDbTable ShippingDocumentToFiles { get; set; }
|
public ShippingDocumentToFilesDbTable ShippingDocumentToFiles { get; set; }
|
||||||
public StockQuantityHistoryDtoDbTable StockQuantityHistoryDtos { get; set; }
|
public StockQuantityHistoryDtoDbTable StockQuantityHistoryDtos { get; set; }
|
||||||
|
public CustomerCreditDbTable CustomerCredits { get; set; }
|
||||||
|
|
||||||
public IRepository<Customer> Customers { get; set; }
|
public IRepository<Customer> Customers { get; set; }
|
||||||
public IRepository<CustomerRole> CustomerRoles { get; set; }
|
public IRepository<CustomerRole> CustomerRoles { get; set; }
|
||||||
|
|
@ -79,7 +81,7 @@ public class FruitBankDbContext : MgDbContextBase,
|
||||||
PartnerDbTable partnerDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable,
|
PartnerDbTable partnerDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, ShippingItemDbTable shippingItemDbTable,
|
||||||
ShippingItemPalletDbTable shippingItemPalletDbTable, FilesDbTable filesDbTable, ShippingDocumentToFilesDbTable shippingDocumentToFilesDbTable,
|
ShippingItemPalletDbTable shippingItemPalletDbTable, FilesDbTable filesDbTable, ShippingDocumentToFilesDbTable shippingDocumentToFilesDbTable,
|
||||||
ProductDtoDbTable productDtoDbTable, OrderDtoDbTable orderDtoDbTable, OrderItemDtoDbTable orderItemDtoDbTable, OrderItemPalletDbTable orderItemPalletDbTable,
|
ProductDtoDbTable productDtoDbTable, OrderDtoDbTable orderDtoDbTable, OrderItemDtoDbTable orderItemDtoDbTable, OrderItemPalletDbTable orderItemPalletDbTable,
|
||||||
StockQuantityHistoryDtoDbTable stockQuantityHistoryDtos,
|
StockQuantityHistoryDtoDbTable stockQuantityHistoryDtos, CustomerCreditDbTable customerCreditDbTable,
|
||||||
IProductService productService, IStaticCacheManager staticCacheManager,
|
IProductService productService, IStaticCacheManager staticCacheManager,
|
||||||
IRepository<Order> orderRepository,
|
IRepository<Order> orderRepository,
|
||||||
IRepository<OrderItem> orderItemRepository,
|
IRepository<OrderItem> orderItemRepository,
|
||||||
|
|
@ -127,6 +129,7 @@ public class FruitBankDbContext : MgDbContextBase,
|
||||||
StockQuantityHistories = stockQuantityHistories;
|
StockQuantityHistories = stockQuantityHistories;
|
||||||
StockQuantityHistoriesExt = stockQuantityHistoriesExt;
|
StockQuantityHistoriesExt = stockQuantityHistoriesExt;
|
||||||
StockQuantityHistoryDtos = stockQuantityHistoryDtos;
|
StockQuantityHistoryDtos = stockQuantityHistoryDtos;
|
||||||
|
CustomerCredits = customerCreditDbTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IQueryable<Customer> GetCustomersBySystemRoleName(string systemRoleName)
|
public IQueryable<Customer> GetCustomersBySystemRoleName(string systemRoleName)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
|
using Mango.Nop.Data.Interfaces;
|
||||||
|
using Nop.Data;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer.Interfaces;
|
||||||
|
|
||||||
|
public interface ICustomerCreditDbSet<TDbTable> : IMgDbTableBase where TDbTable : IRepository<CustomerCredit>
|
||||||
|
{
|
||||||
|
public TDbTable CustomerCredits { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -331,7 +331,9 @@ public class MgOrderModelFactory<TOrderListModelExt, TOrderModelExt> : OrderMode
|
||||||
public virtual async Task<TOrderListModelExt> PrepareOrderListModelExtendedAsync(OrderSearchModelExtended searchModel, Func<OrderListModel, TOrderModelExt, Task> dataItemCopiedCallback)
|
public virtual async Task<TOrderListModelExt> PrepareOrderListModelExtendedAsync(OrderSearchModelExtended searchModel, Func<OrderListModel, TOrderModelExt, Task> dataItemCopiedCallback)
|
||||||
{
|
{
|
||||||
var customerCompany = searchModel.BillingCompany;
|
var customerCompany = searchModel.BillingCompany;
|
||||||
var customer = await _customerService.GetCustomerByIdAsync(Convert.ToInt32(customerCompany));
|
var customer = int.TryParse(customerCompany, out var customerId) && customerId > 0
|
||||||
|
? await _customerService.GetCustomerByIdAsync(customerId)
|
||||||
|
: null;
|
||||||
//var customer = customers.FirstOrDefault(c => c.Company != null && c.Company.Equals(customerCompany, StringComparison.InvariantCultureIgnoreCase));
|
//var customer = customers.FirstOrDefault(c => c.Company != null && c.Company.Equals(customerCompany, StringComparison.InvariantCultureIgnoreCase));
|
||||||
//var customer = customers.FirstOrDefault(c => c.Company != null && c.Company.Equals(customerCompany, StringComparison.InvariantCultureIgnoreCase));
|
//var customer = customers.FirstOrDefault(c => c.Company != null && c.Company.Equals(customerCompany, StringComparison.InvariantCultureIgnoreCase));
|
||||||
OrderListModel prefiltered;
|
OrderListModel prefiltered;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@ using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Nop.Plugin.Misc.FruitBankPlugin.Components;
|
using Nop.Plugin.Misc.FruitBankPlugin.Components;
|
||||||
|
using Nop.Core.Domain.Messages;
|
||||||
using Nop.Services.Cms;
|
using Nop.Services.Cms;
|
||||||
using Nop.Services.Configuration;
|
using Nop.Services.Configuration;
|
||||||
using Nop.Services.Localization;
|
using Nop.Services.Localization;
|
||||||
|
using Nop.Services.Messages;
|
||||||
using Nop.Services.Plugins;
|
using Nop.Services.Plugins;
|
||||||
using Nop.Services.Security;
|
using Nop.Services.Security;
|
||||||
using Nop.Web.Framework.Infrastructure;
|
using Nop.Web.Framework.Infrastructure;
|
||||||
using Nop.Web.Framework.Menu;
|
using Nop.Web.Framework.Menu;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||||
|
|
||||||
|
|
||||||
namespace Nop.Plugin.Misc.FruitBankPlugin
|
namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
|
|
@ -28,6 +31,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
protected readonly ILocalizationService _localizationService;
|
protected readonly ILocalizationService _localizationService;
|
||||||
protected readonly IUrlHelperFactory _urlHelperFactory;
|
protected readonly IUrlHelperFactory _urlHelperFactory;
|
||||||
private readonly IAdminMenu _adminMenu;
|
private readonly IAdminMenu _adminMenu;
|
||||||
|
private readonly IMessageTemplateService _messageTemplateService;
|
||||||
|
|
||||||
//handle AdminMenuCreatedEvent
|
//handle AdminMenuCreatedEvent
|
||||||
|
|
||||||
|
|
@ -38,7 +42,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
ILocalizationService localizationService,
|
ILocalizationService localizationService,
|
||||||
IPermissionService permissionService,
|
IPermissionService permissionService,
|
||||||
IUrlHelperFactory urlHelperFactory,
|
IUrlHelperFactory urlHelperFactory,
|
||||||
IAdminMenu adminMenu)
|
IAdminMenu adminMenu,
|
||||||
|
IMessageTemplateService messageTemplateService)
|
||||||
{
|
{
|
||||||
_actionContextAccessor = actionContextAccessor;
|
_actionContextAccessor = actionContextAccessor;
|
||||||
_settingService = settingService;
|
_settingService = settingService;
|
||||||
|
|
@ -47,6 +52,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
_urlHelperFactory = urlHelperFactory;
|
_urlHelperFactory = urlHelperFactory;
|
||||||
_adminMenu = adminMenu;
|
_adminMenu = adminMenu;
|
||||||
_permissionService = permissionService;
|
_permissionService = permissionService;
|
||||||
|
_messageTemplateService = messageTemplateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- INSTALL ---
|
// --- INSTALL ---
|
||||||
|
|
@ -192,6 +198,91 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.QuickOrder.InvalidProductOrQuantity", "Invalid product or quantity", en);
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.QuickOrder.InvalidProductOrQuantity", "Invalid product or quantity", en);
|
||||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.QuickOrder.InvalidProductOrQuantity", "\u00c9rv\u00e9nytelen term\u00e9k vagy mennyis\u00e9g", hu);
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.QuickOrder.InvalidProductOrQuantity", "\u00c9rv\u00e9nytelen term\u00e9k vagy mennyis\u00e9g", hu);
|
||||||
|
|
||||||
|
// ── Customer Credit ────────────────────────────────────────────────────
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle", "Customer Credit Management", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle", "\u00dcgyf\u00e9l hitelkeret kezel\u00e9s", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.BackToCustomer", "Back to customer", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.BackToCustomer", "Vissza az \u00fcgyf\u00e9lhez", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.EditTitle", "Set Credit Limit", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.EditTitle", "Hitelkeret be\u00e1ll\u00edt\u00e1sa", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit", "Credit Limit", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit", "Hitelkeret", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimitHint", "Set to 0 to block all orders. Leave the record absent to allow unlimited.", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimitHint", "0 eset\u00e9n minden rendel\u00e9s le van tiltva. Ha nincs rekord, a limit korl\u00e1tlan.", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance", "Outstanding Balance", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance", "Kintlév\u0151 egyenleg", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit", "Remaining Credit", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit", "Szabad keret", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited", "Unlimited", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited", "Korl\u00e1tlan", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.Comment", "Notes", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.Comment", "Megjegyz\u00e9s", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.Save", "Save", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.Save", "Ment\u00e9s", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.UnpaidOrdersTitle", "Unpaid / Pending Orders", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.UnpaidOrdersTitle", "Kifizetetlen / f\u00fcgg\u0151 rendel\u00e9sek", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.NoUnpaidOrders", "No unpaid orders.", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.NoUnpaidOrders", "Nincs kifizetetlen rendel\u00e9s.", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderId", "Order #", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderId", "Rendel\u00e9s #", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderDate", "Date", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderDate", "D\u00e1tum", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderTotal", "Total", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderTotal", "\u00d6sszeg", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderStatus", "Order Status", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderStatus", "Rendel\u00e9s \u00e1llapot", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.PaymentStatus", "Payment Status", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.PaymentStatus", "Fizet\u00e9si \u00e1llapot", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.Total", "Total", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.Total", "\u00d6sszesen", hu);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderBlocked", "Your order cannot be placed because your outstanding balance has reached your credit limit. Please settle your existing balance first.", en);
|
||||||
|
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderBlocked", "A rendel\u00e9st nem lehet leadni, mert a kintlév\u0151 egyenlege el\u00e9rte a hitelkeret\u00e9t. K\u00e9rj\u00fck, el\u0151sz\u00f6r rendezze meglév\u0151 tartoz\u00e1s\u00e1t.", hu);
|
||||||
|
|
||||||
|
// ── Order Started email template ────────────────────────────────────
|
||||||
|
var existingStartedTemplate = await _messageTemplateService
|
||||||
|
.GetMessageTemplatesByNameAsync(FruitBankNotificationService.ORDER_STARTED_TEMPLATE_NAME, 0);
|
||||||
|
if (!existingStartedTemplate.Any())
|
||||||
|
{
|
||||||
|
await _messageTemplateService.InsertMessageTemplateAsync(new MessageTemplate
|
||||||
|
{
|
||||||
|
Name = FruitBankNotificationService.ORDER_STARTED_TEMPLATE_NAME,
|
||||||
|
Subject = "%Store.Name% - Rendelésed feldolgozás alatt (#%Order.OrderNumber%)",
|
||||||
|
Body = "<p>Kedves %Order.CustomerFullName%,</p>" +
|
||||||
|
"<p>Rendelésedet (<strong>#%Order.OrderNumber%</strong>) elkezdtük feldolgozni.</p>" +
|
||||||
|
"%Order.MeasurableNote%" +
|
||||||
|
"<p>Amint elkészül, értesítünk!</p>" +
|
||||||
|
"<p>%Store.Name% csapata</p>",
|
||||||
|
IsActive = true,
|
||||||
|
EmailAccountId = 0,
|
||||||
|
LimitedToStores = false,
|
||||||
|
AllowDirectReply = false,
|
||||||
|
AttachedDownloadId = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Order Audited email template ─────────────────────────────────────
|
||||||
|
var existingTemplate = await _messageTemplateService
|
||||||
|
.GetMessageTemplatesByNameAsync(FruitBankNotificationService.ORDER_AUDITED_TEMPLATE_NAME, 0);
|
||||||
|
if (existingTemplate.Count == 0)
|
||||||
|
{
|
||||||
|
await _messageTemplateService.InsertMessageTemplateAsync(new MessageTemplate
|
||||||
|
{
|
||||||
|
Name = FruitBankNotificationService.ORDER_AUDITED_TEMPLATE_NAME,
|
||||||
|
Subject = "%Store.Name% - Rendelésed elkészült (#%Order.OrderNumber%)",
|
||||||
|
Body = "<p>Kedves %Order.CustomerFullName%,</p>" +
|
||||||
|
"<p>Rendelésed (<strong>#%Order.OrderNumber%</strong>) elkészült és átvételre vár.</p>" +
|
||||||
|
"%Order.MeasurableNote%" +
|
||||||
|
"<p>Végleges összeg: <strong>%Order.OrderTotal%</strong></p>" +
|
||||||
|
"<p>Köszönjük a rendelésedet!</p>" +
|
||||||
|
"<p>%Store.Name% csapata</p>",
|
||||||
|
IsActive = true,
|
||||||
|
EmailAccountId = 0, // 0 = use store default
|
||||||
|
LimitedToStores = false,
|
||||||
|
AllowDirectReply = false,
|
||||||
|
AttachedDownloadId = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await base.InstallAsync();
|
await base.InstallAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,7 +298,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
|
|
||||||
public Task<IList<string>> GetWidgetZonesAsync()
|
public Task<IList<string>> GetWidgetZonesAsync()
|
||||||
{
|
{
|
||||||
return Task.FromResult<IList<string>>(new List<string> { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock, AdminWidgetZones.OrderDetailsBlock });
|
return Task.FromResult<IList<string>>(new List<string>
|
||||||
|
{
|
||||||
|
PublicWidgetZones.ProductBoxAddinfoBefore,
|
||||||
|
PublicWidgetZones.ProductDetailsBottom,
|
||||||
|
AdminWidgetZones.ProductDetailsBlock,
|
||||||
|
AdminWidgetZones.OrderDetailsBlock,
|
||||||
|
AdminWidgetZones.CustomerDetailsBlock
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ManageSiteMapAsync(AdminMenuItem rootNode)
|
public async Task ManageSiteMapAsync(AdminMenuItem rootNode)
|
||||||
|
|
@ -232,15 +330,19 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
{
|
{
|
||||||
if (widgetZone == PublicWidgetZones.ProductBoxAddinfoBefore || widgetZone == PublicWidgetZones.ProductDetailsBottom)
|
if (widgetZone == PublicWidgetZones.ProductBoxAddinfoBefore || widgetZone == PublicWidgetZones.ProductDetailsBottom)
|
||||||
{
|
{
|
||||||
return zones.Any(widgetZone.Equals) ? typeof(ProductAIWidgetViewComponent) : null;
|
return typeof(ProductAIWidgetViewComponent);
|
||||||
}
|
}
|
||||||
else if (widgetZone == AdminWidgetZones.ProductDetailsBlock)
|
else if (widgetZone == AdminWidgetZones.ProductDetailsBlock)
|
||||||
{
|
{
|
||||||
return zones.Any(widgetZone.Equals) ? typeof(ProductAttributesViewComponent) : null;
|
return typeof(ProductAttributesViewComponent);
|
||||||
}
|
}
|
||||||
else if (widgetZone == AdminWidgetZones.OrderDetailsBlock)
|
else if (widgetZone == AdminWidgetZones.OrderDetailsBlock)
|
||||||
{
|
{
|
||||||
return zones.Any(widgetZone.Equals) ? typeof(OrderAttributesViewComponent) : null;
|
return typeof(OrderAttributesViewComponent);
|
||||||
|
}
|
||||||
|
else if (widgetZone == AdminWidgetZones.CustomerDetailsBlock)
|
||||||
|
{
|
||||||
|
return typeof(CustomerCreditWidgetViewComponent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ public class PluginNopStartup : INopStartup
|
||||||
services.AddScoped<StockTakingDbTable>();
|
services.AddScoped<StockTakingDbTable>();
|
||||||
services.AddScoped<StockTakingItemDbTable>();
|
services.AddScoped<StockTakingItemDbTable>();
|
||||||
services.AddScoped<StockTakingItemPalletDbTable>();
|
services.AddScoped<StockTakingItemPalletDbTable>();
|
||||||
|
services.AddScoped<CustomerCreditDbTable>();
|
||||||
|
|
||||||
services.AddScoped<StockTakingDbContext>();
|
services.AddScoped<StockTakingDbContext>();
|
||||||
services.AddScoped<FruitBankDbContext>();
|
services.AddScoped<FruitBankDbContext>();
|
||||||
|
|
@ -127,8 +128,7 @@ public class PluginNopStartup : INopStartup
|
||||||
//services.AddScoped<IAIAPIService, OpenAIApiService>();
|
//services.AddScoped<IAIAPIService, OpenAIApiService>();
|
||||||
services.AddScoped<AICalculationService>();
|
services.AddScoped<AICalculationService>();
|
||||||
services.AddScoped<PdfToImageService>();
|
services.AddScoped<PdfToImageService>();
|
||||||
services.AddScoped<IWorkflowMessageService, WorkflowMessageService>();
|
services.AddScoped<FruitBankNotificationService>();
|
||||||
|
|
||||||
services.AddSingleton<IFileStorageProvider>(sp =>
|
services.AddSingleton<IFileStorageProvider>(sp =>
|
||||||
new LocalFileStorageProvider() // Uses default wwwroot/uploads
|
new LocalFileStorageProvider() // Uses default wwwroot/uploads
|
||||||
// Or specify custom path:
|
// Or specify custom path:
|
||||||
|
|
@ -137,6 +137,7 @@ public class PluginNopStartup : INopStartup
|
||||||
|
|
||||||
// Register the file storage service
|
// Register the file storage service
|
||||||
services.AddScoped<FileStorageService>();
|
services.AddScoped<FileStorageService>();
|
||||||
|
services.AddScoped<ICustomerCreditService, CustomerCreditService>();
|
||||||
|
|
||||||
services.AddControllersWithViews(options =>
|
services.AddControllersWithViews(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -21,58 +21,60 @@ public class RouteProvider : IRouteProvider
|
||||||
pattern: "Admin/FruitBankPlugin/Configure",
|
pattern: "Admin/FruitBankPlugin/Configure",
|
||||||
defaults: new { controller = "FruitBankPluginAdmin", action = "Configure", area = AreaNames.ADMIN });
|
defaults: new { controller = "FruitBankPluginAdmin", action = "Configure", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
//endpointRouteBuilder.MapHub<FruitBankHub>("/fbhub");//.RequireCors("AllowBlazorClient");
|
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Order.List",
|
name: "Plugin.FruitBank.Admin.Order.List",
|
||||||
pattern: "Admin/Order/List",
|
pattern: "Admin/Order/List",
|
||||||
defaults: new { controller = "CustomOrder", action = "List", area = AreaNames.ADMIN }
|
defaults: new { controller = "CustomOrder", action = "NewList", area = AreaNames.ADMIN });
|
||||||
);
|
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Order.OrderList",
|
name: "Plugin.FruitBank.Admin.Order.OrderList",
|
||||||
pattern: "Admin/Order/OrderList",
|
pattern: "Admin/Order/OrderList",
|
||||||
defaults: new { controller = "CustomOrder", action = "OrderList", area = AreaNames.ADMIN });
|
defaults: new { controller = "CustomOrder", action = "OrderList", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
|
name: "Plugin.FruitBank.Admin.Order.FruitBankOrderList",
|
||||||
|
pattern: "Admin/CustomOrder/FruitBankOrderList",
|
||||||
|
defaults: new { controller = "CustomOrder", action = "FruitBankOrderList", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
|
name: "Plugin.FruitBank.Admin.Order.UpdateOrderField",
|
||||||
|
pattern: "Admin/CustomOrder/UpdateOrderField",
|
||||||
|
defaults: new { controller = "CustomOrder", action = "UpdateOrderField", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Order.Test",
|
name: "Plugin.FruitBank.Admin.Order.Test",
|
||||||
pattern: "Admin/Order/Test",
|
pattern: "Admin/Order/Test",
|
||||||
defaults: new { controller = "CustomOrder", action = "Test", area = AreaNames.ADMIN }
|
defaults: new { controller = "CustomOrder", action = "Test", area = AreaNames.ADMIN });
|
||||||
);
|
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Index",
|
name: "Plugin.FruitBank.Admin.Index",
|
||||||
pattern: "Admin",
|
pattern: "Admin",
|
||||||
defaults: new { controller = "CustomDashboard", action = "Index", area = AreaNames.ADMIN }
|
defaults: new { controller = "CustomDashboard", action = "Index", area = AreaNames.ADMIN });
|
||||||
);
|
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Shipping.List",
|
name: "Plugin.FruitBank.Admin.Shipping.List",
|
||||||
pattern: "Admin/Shipping/List",
|
pattern: "Admin/Shipping/List",
|
||||||
defaults: new { controller = "Shipping", action = "List", area = AreaNames.ADMIN }
|
defaults: new { controller = "Shipping", action = "List", area = AreaNames.ADMIN });
|
||||||
);
|
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Shipping.ShippingList",
|
name: "Plugin.FruitBank.Admin.Shipping.ShippingList",
|
||||||
pattern: "Admin/Shipping/ShippingList",
|
pattern: "Admin/Shipping/ShippingList",
|
||||||
defaults: new { controller = "Shipping", action = "ShippingList", area = AreaNames.ADMIN }
|
defaults: new { controller = "Shipping", action = "ShippingList", area = AreaNames.ADMIN });
|
||||||
);
|
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Invoices.List",
|
name: "Plugin.FruitBank.Admin.Invoices.List",
|
||||||
pattern: "Admin/Invoices/List",
|
pattern: "Admin/Invoices/List",
|
||||||
defaults: new { controller = "Invoice", action = "List", area = AreaNames.ADMIN }
|
defaults: new { controller = "Invoice", action = "List", area = AreaNames.ADMIN });
|
||||||
);
|
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Shipping.Create",
|
name: "Plugin.FruitBank.Admin.Shipping.Create",
|
||||||
pattern: "Admin/Shipping/Create",
|
pattern: "Admin/Shipping/Create",
|
||||||
defaults: new { controller = "Shipping", action = "Create", area = AreaNames.ADMIN });
|
defaults: new { controller = "Shipping", action = "Create", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Shipping.Edit",
|
name: "Plugin.FruitBank.Admin.Shipping.Edit",
|
||||||
pattern: "Admin/Shipping/Edit",
|
pattern: "Admin/Shipping/Edit",
|
||||||
defaults: new { controller = "Shipping", action = "Edit", area = AreaNames.ADMIN });
|
defaults: new { controller = "Shipping", action = "Edit", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Shipping.UploadFile",
|
name: "Plugin.FruitBank.Admin.Shipping.UploadFile",
|
||||||
|
|
@ -150,9 +152,9 @@ public class RouteProvider : IRouteProvider
|
||||||
defaults: new { controller = "CustomOrder", action = "Edit", area = AreaNames.ADMIN });
|
defaults: new { controller = "CustomOrder", action = "Edit", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.Order.AddProduct",
|
name: "Plugin.FruitBank.Admin.Order.AddProduct",
|
||||||
pattern: "Admin/CustomOrder/FruitBankAddProductToOrder",
|
pattern: "Admin/CustomOrder/FruitBankAddProductToOrder",
|
||||||
defaults: new { controller = "CustomOrder", action = "FruitBankAddProductToOrder", area = AreaNames.ADMIN });
|
defaults: new { controller = "CustomOrder", action = "FruitBankAddProductToOrder", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.Admin.ManagementPage.ProcessShippingDocument",
|
name: "Plugin.FruitBank.Admin.ManagementPage.ProcessShippingDocument",
|
||||||
|
|
@ -179,6 +181,22 @@ public class RouteProvider : IRouteProvider
|
||||||
pattern: "Admin/ExtractText",
|
pattern: "Admin/ExtractText",
|
||||||
defaults: new { controller = "FileManager", action = "ImageTextExtraction", area = AreaNames.ADMIN });
|
defaults: new { controller = "FileManager", action = "ImageTextExtraction", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
|
// ── Customer Credit ──────────────────────────────────────────────────
|
||||||
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
|
name: "Plugin.FruitBank.Admin.CustomerCredit.List",
|
||||||
|
pattern: "Admin/CustomerCredit/List",
|
||||||
|
defaults: new { controller = "CustomerCredit", action = "List", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
|
name: "Plugin.FruitBank.Admin.CustomerCredit.CustomerCreditList",
|
||||||
|
pattern: "Admin/CustomerCredit/CustomerCreditList",
|
||||||
|
defaults: new { controller = "CustomerCredit", action = "CustomerCreditList", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
|
name: "Plugin.FruitBank.Admin.CustomerCredit.UpdateCreditLimit",
|
||||||
|
pattern: "Admin/CustomerCredit/UpdateCreditLimit",
|
||||||
|
defaults: new { controller = "CustomerCredit", action = "UpdateCreditLimit", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
// ── Public: Quick Order ──────────────────────────────────────────────
|
// ── Public: Quick Order ──────────────────────────────────────────────
|
||||||
endpointRouteBuilder.MapControllerRoute(
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
name: "Plugin.FruitBank.QuickOrder.Index",
|
name: "Plugin.FruitBank.QuickOrder.Index",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Language Name="English" IsDefault="false" IsRightToLeft="false">
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
Customer Credit — Plugins.Misc.FruitBankPlugin.CustomerCredit.*
|
||||||
|
Import: Admin > Configuration > Languages > [English] > Import resources
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Page -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle">
|
||||||
|
<Value><![CDATA[Customer Credit Management]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.BackToCustomer">
|
||||||
|
<Value><![CDATA[Back to customer]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.EditTitle">
|
||||||
|
<Value><![CDATA[Set Credit Limit]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Summary cards -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit">
|
||||||
|
<Value><![CDATA[Credit Limit]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimitHint">
|
||||||
|
<Value><![CDATA[Set to 0 to block all orders. Leave the record absent to allow unlimited.]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance">
|
||||||
|
<Value><![CDATA[Outstanding Balance]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit">
|
||||||
|
<Value><![CDATA[Remaining Credit]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited">
|
||||||
|
<Value><![CDATA[Unlimited]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.Comment">
|
||||||
|
<Value><![CDATA[Notes]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.Save">
|
||||||
|
<Value><![CDATA[Save]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- List page -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.CustomerName">
|
||||||
|
<Value><![CDATA[Customer Name]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.CustomerEmail">
|
||||||
|
<Value><![CDATA[Email]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Unpaid orders table -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.UnpaidOrdersTitle">
|
||||||
|
<Value><![CDATA[Unpaid / Pending Orders]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.NoUnpaidOrders">
|
||||||
|
<Value><![CDATA[No unpaid orders.]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderId">
|
||||||
|
<Value><![CDATA[Order #]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderDate">
|
||||||
|
<Value><![CDATA[Date]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderTotal">
|
||||||
|
<Value><![CDATA[Total]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderStatus">
|
||||||
|
<Value><![CDATA[Order Status]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.PaymentStatus">
|
||||||
|
<Value><![CDATA[Payment Status]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.Total">
|
||||||
|
<Value><![CDATA[Total]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Enforcement -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderBlocked">
|
||||||
|
<Value><![CDATA[Your order cannot be placed because your outstanding balance has reached your credit limit. Please settle your existing balance first.]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
</Language>
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Language Name="Hungarian" IsDefault="false" IsRightToLeft="false">
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
Customer Credit — Plugins.Misc.FruitBankPlugin.CustomerCredit.*
|
||||||
|
Import: Admin > Konfiguráció > Nyelvek > [Magyar] > Erőforrások importálása
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Oldal -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle">
|
||||||
|
<Value><![CDATA[Ügyfél hitelkeret kezelés]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.BackToCustomer">
|
||||||
|
<Value><![CDATA[Vissza az ügyfélhez]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.EditTitle">
|
||||||
|
<Value><![CDATA[Hitelkeret beállítása]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Összefoglaló kártyák -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit">
|
||||||
|
<Value><![CDATA[Hitelkeret]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimitHint">
|
||||||
|
<Value><![CDATA[0 esetén minden rendelés le van tiltva. Ha nincs rekord, a limit korlátlan.]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance">
|
||||||
|
<Value><![CDATA[Kintlévő egyenleg]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit">
|
||||||
|
<Value><![CDATA[Szabad keret]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited">
|
||||||
|
<Value><![CDATA[Korlátlan]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Űrlap -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.Comment">
|
||||||
|
<Value><![CDATA[Megjegyzés]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.Save">
|
||||||
|
<Value><![CDATA[Mentés]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Lista oldal -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.CustomerName">
|
||||||
|
<Value><![CDATA[Ügyfél neve]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.CustomerEmail">
|
||||||
|
<Value><![CDATA[Email]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Kifizetetlen rendelések táblázat -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.UnpaidOrdersTitle">
|
||||||
|
<Value><![CDATA[Kifizetetlen / függő rendelések]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.NoUnpaidOrders">
|
||||||
|
<Value><![CDATA[Nincs kifizetetlen rendelés.]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderId">
|
||||||
|
<Value><![CDATA[Rendelés #]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderDate">
|
||||||
|
<Value><![CDATA[Dátum]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderTotal">
|
||||||
|
<Value><![CDATA[Összeg]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderStatus">
|
||||||
|
<Value><![CDATA[Rendelés állapot]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.PaymentStatus">
|
||||||
|
<Value><![CDATA[Fizetési állapot]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.Total">
|
||||||
|
<Value><![CDATA[Összesen]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
<!-- Hitelkeret túllépés hibaüzenet -->
|
||||||
|
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerCredit.OrderBlocked">
|
||||||
|
<Value><![CDATA[A rendelést nem lehet leadni, mert a kintlévő egyenlege elérte a hitelkeretét. Kérjük, először rendezze meglévő tartozását.]]></Value>
|
||||||
|
</LocaleResource>
|
||||||
|
|
||||||
|
</Language>
|
||||||
|
|
@ -42,6 +42,7 @@ public partial class NameCompatibility : INameCompatibility
|
||||||
{ typeof(StockTaking), FruitBankConstClient.StockTakingDbTableName},
|
{ typeof(StockTaking), FruitBankConstClient.StockTakingDbTableName},
|
||||||
{ typeof(StockTakingItem), FruitBankConstClient.StockTakingItemDbTableName},
|
{ typeof(StockTakingItem), FruitBankConstClient.StockTakingItemDbTableName},
|
||||||
{ typeof(StockTakingItemPallet), FruitBankConstClient.StockTakingItemPalletDbTableName},
|
{ typeof(StockTakingItemPallet), FruitBankConstClient.StockTakingItemPalletDbTableName},
|
||||||
|
{ typeof(CustomerCredit), FruitBankConstClient.CustomerCreditDbTableName},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
using Nop.Web.Framework.Models;
|
||||||
|
using Nop.Web.Framework.Mvc.ModelBinding;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Models;
|
||||||
|
|
||||||
|
public record CustomerCreditWidgetModel : BaseNopModel
|
||||||
|
{
|
||||||
|
public int CustomerId { get; set; }
|
||||||
|
public bool HasCreditLimit { get; set; }
|
||||||
|
|
||||||
|
[NopResourceDisplayName("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit")]
|
||||||
|
public decimal CreditLimit { get; set; }
|
||||||
|
|
||||||
|
[NopResourceDisplayName("Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance")]
|
||||||
|
public decimal OutstandingBalance { get; set; }
|
||||||
|
|
||||||
|
[NopResourceDisplayName("Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit")]
|
||||||
|
public decimal? RemainingCredit { get; set; }
|
||||||
|
|
||||||
|
public string? Comment { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Orders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lightweight DTO returned by the FruitBankOrderList endpoint.
|
||||||
|
/// Contains only what the grid needs – avoids serialising heavy OrderModel navigation properties.
|
||||||
|
/// </summary>
|
||||||
|
public record FruitBankOrderRowDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string CustomOrderNumber { get; init; }
|
||||||
|
public string CustomerCompany { get; init; }
|
||||||
|
public int CustomerId { get; init; }
|
||||||
|
|
||||||
|
// FruitBank-specific fields
|
||||||
|
public string InnvoiceTechId { get; init; }
|
||||||
|
public bool IsAllOrderItemAvgWeightValid { get; init; }
|
||||||
|
public bool IsMeasurable { get; init; }
|
||||||
|
public int MeasuringStatus { get; init; }
|
||||||
|
public string MeasuringStatusString { get; init; }
|
||||||
|
public DateTime? DateOfReceipt { get; init; }
|
||||||
|
|
||||||
|
// NopCommerce order fields
|
||||||
|
public int OrderStatusId { get; init; }
|
||||||
|
public string OrderStatus { get; init; }
|
||||||
|
public int PaymentStatusId { get; init; }
|
||||||
|
public string PaymentStatus { get; init; }
|
||||||
|
public int ShippingStatusId { get; init; }
|
||||||
|
public string ShippingStatus { get; init; }
|
||||||
|
public string StoreName { get; init; }
|
||||||
|
public DateTime CreatedOn { get; init; }
|
||||||
|
public string OrderTotal { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -173,6 +173,12 @@
|
||||||
<None Update="Areas\Admin\Views\AppDownload\Index.cshtml">
|
<None Update="Areas\Admin\Views\AppDownload\Index.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Areas\Admin\Views\CustomerCredit\Details.cshtml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Areas\Admin\Views\CustomerCredit\List.cshtml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="Areas\Admin\Views\Extras\ImageTextExtraction.cshtml">
|
<None Update="Areas\Admin\Views\Extras\ImageTextExtraction.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|
@ -185,6 +191,9 @@
|
||||||
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
|
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Areas\Admin\Views\Order\FruitBankOrderList.cshtml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="Areas\Admin\Views\Order\TestGridComponent.cshtml">
|
<None Update="Areas\Admin\Views\Order\TestGridComponent.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|
@ -650,6 +659,9 @@
|
||||||
<None Update="Views\Checkout\PendingMeasurementWarning.cshtml">
|
<None Update="Views\Checkout\PendingMeasurementWarning.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Views\CustomerCreditWidget.cshtml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="Views\ProductAIListWidget.cshtml">
|
<None Update="Views\ProductAIListWidget.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
|
using Nop.Core.Domain.Orders;
|
||||||
|
using Nop.Core.Domain.Payments;
|
||||||
|
using Nop.Data;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||||
|
|
||||||
|
public class CustomerCreditService : ICustomerCreditService
|
||||||
|
{
|
||||||
|
private readonly CustomerCreditDbTable _customerCreditDbTable;
|
||||||
|
private readonly IRepository<Order> _orderRepository;
|
||||||
|
|
||||||
|
public CustomerCreditService(
|
||||||
|
CustomerCreditDbTable customerCreditDbTable,
|
||||||
|
IRepository<Order> orderRepository)
|
||||||
|
{
|
||||||
|
_customerCreditDbTable = customerCreditDbTable;
|
||||||
|
_orderRepository = orderRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CustomerCredit?> GetByCustomerIdAsync(int customerId)
|
||||||
|
=> _customerCreditDbTable.GetByCustomerIdAsync(customerId);
|
||||||
|
|
||||||
|
public async Task SaveAsync(CustomerCredit entity)
|
||||||
|
{
|
||||||
|
entity.UpdatedOnUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (entity.Id <= 0)
|
||||||
|
{
|
||||||
|
entity.CreatedOnUtc = DateTime.UtcNow;
|
||||||
|
await _customerCreditDbTable.InsertAsync(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _customerCreditDbTable.UpdateAsync(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteAsync(CustomerCredit entity)
|
||||||
|
=> _customerCreditDbTable.DeleteAsync(entity);
|
||||||
|
|
||||||
|
public async Task<decimal> GetOutstandingBalanceAsync(int customerId)
|
||||||
|
{
|
||||||
|
return await _orderRepository.Table
|
||||||
|
.Where(o =>
|
||||||
|
o.CustomerId == customerId &&
|
||||||
|
o.OrderStatusId != (int)OrderStatus.Cancelled &&
|
||||||
|
(o.PaymentStatusId == (int)PaymentStatus.Pending ||
|
||||||
|
o.PaymentStatusId == (int)PaymentStatus.PartiallyRefunded))
|
||||||
|
.SumAsync(o => (decimal?)o.OrderTotal) ?? 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<decimal?> GetRemainingCreditAsync(int customerId)
|
||||||
|
{
|
||||||
|
var credit = await GetByCustomerIdAsync(customerId);
|
||||||
|
if (credit == null) return null;
|
||||||
|
|
||||||
|
var outstanding = await GetOutstandingBalanceAsync(customerId);
|
||||||
|
return credit.CreditLimit - outstanding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsOrderAllowedAsync(int customerId, decimal newOrderTotal)
|
||||||
|
{
|
||||||
|
var credit = await GetByCustomerIdAsync(customerId);
|
||||||
|
if (credit == null) return true;
|
||||||
|
|
||||||
|
var outstanding = await GetOutstandingBalanceAsync(customerId);
|
||||||
|
return outstanding + newOrderTotal <= credit.CreditLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Enums;
|
||||||
|
using FruitBank.Common.Interfaces;
|
||||||
using FruitBank.Common.Server;
|
using FruitBank.Common.Server;
|
||||||
using Mango.Nop.Core.Dtos;
|
using Mango.Nop.Core.Dtos;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -8,6 +9,7 @@ using Nop.Core;
|
||||||
using Nop.Core.Domain.Catalog;
|
using Nop.Core.Domain.Catalog;
|
||||||
using Nop.Core.Domain.Common;
|
using Nop.Core.Domain.Common;
|
||||||
using Nop.Core.Domain.Customers;
|
using Nop.Core.Domain.Customers;
|
||||||
|
using Nop.Core.Domain.Messages;
|
||||||
using Nop.Core.Domain.Orders;
|
using Nop.Core.Domain.Orders;
|
||||||
using Nop.Core.Domain.Tax;
|
using Nop.Core.Domain.Tax;
|
||||||
using Nop.Core.Events;
|
using Nop.Core.Events;
|
||||||
|
|
@ -20,11 +22,13 @@ using Nop.Services.Common;
|
||||||
using Nop.Services.Customers;
|
using Nop.Services.Customers;
|
||||||
using Nop.Services.Events;
|
using Nop.Services.Events;
|
||||||
using Nop.Services.Localization;
|
using Nop.Services.Localization;
|
||||||
|
using Nop.Services.Messages;
|
||||||
using Nop.Services.Orders;
|
using Nop.Services.Orders;
|
||||||
using Nop.Services.Plugins;
|
using Nop.Services.Plugins;
|
||||||
using Nop.Web.Framework.Events;
|
using Nop.Web.Framework.Events;
|
||||||
using Nop.Web.Framework.Menu;
|
using Nop.Web.Framework.Menu;
|
||||||
using Nop.Web.Models.Sitemap;
|
using Nop.Web.Models.Sitemap;
|
||||||
|
using NUglify.JavaScript.Syntax;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
|
@ -47,6 +51,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
private readonly FruitBankDbContext _dbContext;
|
private readonly FruitBankDbContext _dbContext;
|
||||||
private readonly IAttributeParser<CustomerAttribute, CustomerAttributeValue> _attributeParser;
|
private readonly IAttributeParser<CustomerAttribute, CustomerAttributeValue> _attributeParser;
|
||||||
private readonly ICustomerService _customerService;
|
private readonly ICustomerService _customerService;
|
||||||
|
private readonly IWorkflowMessageService _workflowMessageService;
|
||||||
|
private readonly FruitBankNotificationService _fruitBankNotificationService;
|
||||||
|
|
||||||
public EventConsumer(
|
public EventConsumer(
|
||||||
IGenericAttributeService genericAttributeService,
|
IGenericAttributeService genericAttributeService,
|
||||||
|
|
@ -64,7 +70,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
FruitBankAttributeService fruitBankAttributeService,
|
FruitBankAttributeService fruitBankAttributeService,
|
||||||
FruitBankDbContext dbContext,
|
FruitBankDbContext dbContext,
|
||||||
IAttributeParser<CustomerAttribute, CustomerAttributeValue> attributeParser,
|
IAttributeParser<CustomerAttribute, CustomerAttributeValue> attributeParser,
|
||||||
ICustomerService customerService
|
ICustomerService customerService,
|
||||||
|
IWorkflowMessageService workflowMessageService,
|
||||||
|
FruitBankNotificationService fruitBankNotificationService
|
||||||
) : base(pluginManager)
|
) : base(pluginManager)
|
||||||
{
|
{
|
||||||
_genericAttributeService = genericAttributeService;
|
_genericAttributeService = genericAttributeService;
|
||||||
|
|
@ -82,6 +90,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
_attributeParser = attributeParser;
|
_attributeParser = attributeParser;
|
||||||
_customerService = customerService;
|
_customerService = customerService;
|
||||||
|
_workflowMessageService = workflowMessageService;
|
||||||
|
_fruitBankNotificationService = fruitBankNotificationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string PluginSystemName => "Misc.FruitBankPlugin";
|
protected override string PluginSystemName => "Misc.FruitBankPlugin";
|
||||||
|
|
@ -93,6 +103,43 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
public async Task HandleEventAsync(EntityUpdatedEvent<Order> eventMessage)
|
public async Task HandleEventAsync(EntityUpdatedEvent<Order> eventMessage)
|
||||||
{
|
{
|
||||||
await SaveOrderCustomAttributesAsync(eventMessage.Entity);
|
await SaveOrderCustomAttributesAsync(eventMessage.Entity);
|
||||||
|
if (eventMessage.Entity == null) return;
|
||||||
|
|
||||||
|
var orderDto = await _dbContext.OrderDtos.GetByIdAsync(eventMessage.Entity.Id, true);
|
||||||
|
if (orderDto == null) return;
|
||||||
|
|
||||||
|
if (orderDto.MeasuringStatus == MeasuringStatus.Audited)
|
||||||
|
{
|
||||||
|
var alreadySent = await _fruitBankAttributeService
|
||||||
|
.GetGenericAttributeValueAsync<Order, bool>(eventMessage.Entity.Id, "OrderAuditedNotificationSent");
|
||||||
|
|
||||||
|
if (!alreadySent)
|
||||||
|
{
|
||||||
|
await _fruitBankNotificationService
|
||||||
|
.SendOrderAuditedCustomerNotificationAsync(eventMessage.Entity, orderDto.IsMeasurable);
|
||||||
|
|
||||||
|
await _fruitBankAttributeService
|
||||||
|
.InsertOrUpdateGenericAttributeAsync<Order, bool>(
|
||||||
|
eventMessage.Entity.Id, "OrderAuditedNotificationSent", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (orderDto.MeasuringStatus == MeasuringStatus.Started)
|
||||||
|
{
|
||||||
|
var alreadySent = await _fruitBankAttributeService
|
||||||
|
.GetGenericAttributeValueAsync<Order, bool>(eventMessage.Entity.Id, "OrderStartedNotificationSent");
|
||||||
|
|
||||||
|
if (!alreadySent)
|
||||||
|
{
|
||||||
|
await _fruitBankNotificationService
|
||||||
|
.SendOrderStartedCustomerNotificationAsync(eventMessage.Entity, orderDto.IsMeasurable);
|
||||||
|
|
||||||
|
await _fruitBankAttributeService
|
||||||
|
.InsertOrUpdateGenericAttributeAsync<Order, bool>(
|
||||||
|
eventMessage.Entity.Id, "OrderStartedNotificationSent", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
using Nop.Core;
|
||||||
|
using Nop.Core.Domain.Messages;
|
||||||
|
using Nop.Core.Domain.Orders;
|
||||||
|
using Nop.Services.Customers;
|
||||||
|
using Nop.Services.Messages;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||||
|
|
||||||
|
public class FruitBankNotificationService(
|
||||||
|
IMessageTemplateService messageTemplateService,
|
||||||
|
IEmailAccountService emailAccountService,
|
||||||
|
EmailAccountSettings emailAccountSettings,
|
||||||
|
IMessageTokenProvider messageTokenProvider,
|
||||||
|
IWorkflowMessageService workflowMessageService,
|
||||||
|
ICustomerService customerService,
|
||||||
|
IStoreContext storeContext)
|
||||||
|
{
|
||||||
|
public const string ORDER_AUDITED_TEMPLATE_NAME = "FruitBank.OrderAudited.CustomerNotification";
|
||||||
|
public const string ORDER_STARTED_TEMPLATE_NAME = "FruitBank.OrderStarted.CustomerNotification";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the "order started" (being prepared) customer notification.
|
||||||
|
/// For measurable orders, informs the customer that final prices will be
|
||||||
|
/// confirmed after weighing. Fires once when MeasuringStatus transitions to Started.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> SendOrderStartedCustomerNotificationAsync(Order order, bool isMeasurable)
|
||||||
|
{
|
||||||
|
var measurableNote = isMeasurable
|
||||||
|
? "<p>Rendelésed mérhető tételeket tartalmaz. A végleges ár a mérés után kerül megerősítésre.</p>"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
return await SendNotificationAsync(ORDER_STARTED_TEMPLATE_NAME, order, measurableNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the "order audited" customer notification.
|
||||||
|
/// For measurable orders, confirms that weights have been recorded and
|
||||||
|
/// the final price is as shown on the order.
|
||||||
|
/// Fires once when MeasuringStatus transitions to Audited.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> SendOrderAuditedCustomerNotificationAsync(Order order, bool isMeasurable)
|
||||||
|
{
|
||||||
|
var measurableNote = isMeasurable
|
||||||
|
? "<p>A mért tételek súlyait rögzítettük, a végleges ár a rendelésen feltüntetett összeg.</p>"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
return await SendNotificationAsync(ORDER_AUDITED_TEMPLATE_NAME, order, measurableNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── shared core ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task<int> SendNotificationAsync(string templateName, Order order, string measurableNote)
|
||||||
|
{
|
||||||
|
var store = await storeContext.GetCurrentStoreAsync();
|
||||||
|
|
||||||
|
var templates = await messageTemplateService.GetMessageTemplatesByNameAsync(templateName, store.Id);
|
||||||
|
var messageTemplate = templates?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (messageTemplate is null || !messageTemplate.IsActive)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var emailAccount = await emailAccountService.GetEmailAccountByIdAsync(messageTemplate.EmailAccountId)
|
||||||
|
?? await emailAccountService.GetEmailAccountByIdAsync(emailAccountSettings.DefaultEmailAccountId);
|
||||||
|
|
||||||
|
var tokens = new List<Token>();
|
||||||
|
await messageTokenProvider.AddStoreTokensAsync(tokens, store, emailAccount, order.CustomerLanguageId);
|
||||||
|
await messageTokenProvider.AddOrderTokensAsync(tokens, order, order.CustomerLanguageId);
|
||||||
|
|
||||||
|
var customer = await customerService.GetCustomerByIdAsync(order.CustomerId);
|
||||||
|
await messageTokenProvider.AddCustomerTokensAsync(tokens, customer);
|
||||||
|
|
||||||
|
tokens.Add(new Token("Order.MeasurableNote", measurableNote, true));
|
||||||
|
|
||||||
|
var toEmail = customer.Email;
|
||||||
|
var toName = $"{customer.FirstName} {customer.LastName}".Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(toName)) toName = customer.Email;
|
||||||
|
|
||||||
|
return await workflowMessageService.SendNotificationAsync(
|
||||||
|
messageTemplate, emailAccount,
|
||||||
|
order.CustomerLanguageId,
|
||||||
|
tokens,
|
||||||
|
toEmail, toName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||||
|
|
||||||
|
public interface ICustomerCreditService
|
||||||
|
{
|
||||||
|
/// <summary>Gets the credit record for a customer, or null if none exists (= unlimited).</summary>
|
||||||
|
Task<CustomerCredit?> GetByCustomerIdAsync(int customerId);
|
||||||
|
|
||||||
|
/// <summary>Insert or update a customer credit record.</summary>
|
||||||
|
Task SaveAsync(CustomerCredit entity);
|
||||||
|
|
||||||
|
/// <summary>Delete the credit record for a customer, restoring unlimited access.</summary>
|
||||||
|
Task DeleteAsync(CustomerCredit entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sum of OrderTotal for all pending/unpaid, non-cancelled orders for the customer.
|
||||||
|
/// </summary>
|
||||||
|
Task<decimal> GetOutstandingBalanceAsync(int customerId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CreditLimit - OutstandingBalance. Returns null if no credit record exists (= unlimited).
|
||||||
|
/// </summary>
|
||||||
|
Task<decimal?> GetRemainingCreditAsync(int customerId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the customer is allowed to place a new order with the given total.
|
||||||
|
/// Rule: no credit record = always allowed.
|
||||||
|
/// Otherwise: OutstandingBalance + newOrderTotal must be <= CreditLimit.
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> IsOrderAllowedAsync(int customerId, decimal newOrderTotal);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
@model Nop.Plugin.Misc.FruitBankPlugin.Models.CustomerCreditWidgetModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
var remaining = Model.RemainingCredit;
|
||||||
|
var statusClass = !Model.HasCreditLimit ? "text-muted"
|
||||||
|
: remaining <= 0 ? "text-danger"
|
||||||
|
: remaining < Model.CreditLimit * 0.2m ? "text-warning"
|
||||||
|
: "text-success";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="fas fa-credit-card"></i>
|
||||||
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle")
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.CreditLimit")</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<span class="form-control-plaintext">
|
||||||
|
@(Model.HasCreditLimit ? Model.CreditLimit.ToString("N0") + " Ft" : T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited").Text)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.OutstandingBalance")</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<span class="form-control-plaintext">
|
||||||
|
@Model.OutstandingBalance.ToString("N0") Ft
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.RemainingCredit")</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<span class="form-control-plaintext @statusClass">
|
||||||
|
<strong>
|
||||||
|
@if (!Model.HasCreditLimit)
|
||||||
|
{
|
||||||
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Unlimited")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@(remaining!.Value.ToString("N0"))
|
||||||
|
<span>Ft</span>
|
||||||
|
}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Comment))
|
||||||
|
{
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Comment")</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<span class="form-control-plaintext text-muted">@Model.Comment</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-9 offset-md-3">
|
||||||
|
<a href="/Admin/CustomerCredit/Details/@Model.CustomerId" class="btn btn-default">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.EditTitle")
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Reference in New Issue