site, license plate
This commit is contained in:
parent
d568931bd9
commit
d5f151b5ac
|
|
@ -86,4 +86,35 @@ public class CustomerAttributesController : BasePluginController
|
|||
|
||||
return Json(new { success = true, siteCount = entries.Count, defaultAddressId = effectiveDefault });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("Admin/CustomerAttributes/SaveLicensePlates")]
|
||||
public async Task<IActionResult> SaveLicensePlates(int customerId, string[] plates, string? defaultPlate)
|
||||
{
|
||||
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||
return Json(new { success = false, error = "Access denied" });
|
||||
|
||||
var customer = await _customerService.GetCustomerByIdAsync(customerId);
|
||||
if (customer == null)
|
||||
return Json(new { success = false, error = "Customer not found" });
|
||||
|
||||
var cleaned = (plates ?? Array.Empty<string>())
|
||||
.Select(p => (p ?? string.Empty).Trim().ToUpperInvariant())
|
||||
.Where(p => p.Length > 0)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var def = (defaultPlate ?? string.Empty).Trim().ToUpperInvariant();
|
||||
var effectiveDefault = cleaned.Contains(def) ? def : null;
|
||||
|
||||
var entries = cleaned
|
||||
.Select(p => new CustomerLicensePlateEntry { Plate = p, IsDefault = p == effectiveDefault })
|
||||
.ToList();
|
||||
|
||||
var json = JsonSerializer.Serialize(entries);
|
||||
await _attributeService.InsertOrUpdateGenericAttributeAsync<Customer, string>(
|
||||
customerId, FruitBankPluginConst.CustomerLicensePlatesAttribute, json, storeId: 0);
|
||||
|
||||
return Json(new { success = true, plateCount = entries.Count, defaultPlate = effectiveDefault });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Nop.Core.Domain.Customers;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Models;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
using Nop.Services.Customers;
|
||||
using Nop.Services.Orders;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Inline-save endpoints for the FruitBank order attributes (site / license plate)
|
||||
/// on the admin order page (rendered by OrderAttributesViewComponent). Stored as
|
||||
/// store-agnostic (StoreId 0) order generic attributes via <see cref="FruitBankAttributeService"/>,
|
||||
/// using the same keys the OrderDto exposes.
|
||||
/// </summary>
|
||||
[AuthorizeAdmin]
|
||||
[Area(AreaNames.ADMIN)]
|
||||
public class OrderAttributesController : BasePluginController
|
||||
{
|
||||
private readonly FruitBankAttributeService _attributeService;
|
||||
private readonly ICustomerService _customerService;
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly IPermissionService _permissionService;
|
||||
|
||||
public OrderAttributesController(
|
||||
FruitBankAttributeService attributeService,
|
||||
ICustomerService customerService,
|
||||
IOrderService orderService,
|
||||
IPermissionService permissionService)
|
||||
{
|
||||
_attributeService = attributeService;
|
||||
_customerService = customerService;
|
||||
_orderService = orderService;
|
||||
_permissionService = permissionService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("Admin/OrderAttributes/SaveSite")]
|
||||
public async Task<IActionResult> SaveSite(int orderId, int addressId)
|
||||
{
|
||||
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||
return Json(new { success = false, error = "Access denied" });
|
||||
|
||||
var order = await _orderService.GetOrderByIdAsync(orderId);
|
||||
if (order == null) return Json(new { success = false, error = "Rendelés nem található" });
|
||||
|
||||
// Clear
|
||||
if (addressId <= 0)
|
||||
{
|
||||
await _attributeService.DeleteGenericAttributeAsync<Order>(orderId, FruitBankPluginConst.OrderSiteAttribute, 0);
|
||||
return Json(new { success = true, cleared = true });
|
||||
}
|
||||
|
||||
// The chosen address MUST be one of the customer's sites (telephely) — not a
|
||||
// shipping/billing address.
|
||||
var sitesJson = await _attributeService
|
||||
.GetGenericAttributeValueAsync<Customer, string>(order.CustomerId, FruitBankPluginConst.CustomerSitesAttribute, 0);
|
||||
if (!ParseSiteAddressIds(sitesJson).Contains(addressId))
|
||||
return Json(new { success = false, error = "A cím nem a vevő telephelye." });
|
||||
|
||||
var address = (await _customerService.GetAddressesByCustomerIdAsync(order.CustomerId))
|
||||
.FirstOrDefault(a => a.Id == addressId);
|
||||
if (address == null) return Json(new { success = false, error = "Cím nem található" });
|
||||
|
||||
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
|
||||
var snapshot = OrderSiteSnapshotBuilder.FromAddress(address, customer?.VatNumber);
|
||||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(snapshot);
|
||||
|
||||
await _attributeService.InsertOrUpdateGenericAttributeAsync<Order, string>(
|
||||
orderId, FruitBankPluginConst.OrderSiteAttribute, json, 0);
|
||||
|
||||
return Json(new { success = true, display = snapshot.Display });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("Admin/OrderAttributes/SaveLicensePlate")]
|
||||
public async Task<IActionResult> SaveLicensePlate(int orderId, string? licensePlate)
|
||||
{
|
||||
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||
return Json(new { success = false, error = "Access denied" });
|
||||
|
||||
var order = await _orderService.GetOrderByIdAsync(orderId);
|
||||
if (order == null) return Json(new { success = false, error = "Rendelés nem található" });
|
||||
|
||||
var plate = licensePlate?.Trim().ToUpperInvariant();
|
||||
if (string.IsNullOrWhiteSpace(plate))
|
||||
{
|
||||
await _attributeService.DeleteGenericAttributeAsync<Order>(orderId, FruitBankPluginConst.OrderLicensePlateAttribute, 0);
|
||||
return Json(new { success = true, cleared = true });
|
||||
}
|
||||
|
||||
await _attributeService.InsertOrUpdateGenericAttributeAsync<Order, string>(
|
||||
orderId, FruitBankPluginConst.OrderLicensePlateAttribute, plate, 0);
|
||||
|
||||
return Json(new { success = true, licensePlate = plate });
|
||||
}
|
||||
|
||||
private static HashSet<int> ParseSiteAddressIds(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return new();
|
||||
try
|
||||
{
|
||||
var entries = JsonSerializer.Deserialize<List<CustomerSiteEntry>>(json) ?? new();
|
||||
return entries.Select(e => e.AddressId).ToHashSet();
|
||||
}
|
||||
catch { return new(); }
|
||||
}
|
||||
}
|
||||
|
|
@ -50,10 +50,31 @@ public class CustomerCreditWidgetViewComponent : NopViewComponent
|
|||
};
|
||||
|
||||
await PopulateSitesAsync(model, customerId);
|
||||
await PopulatePlatesAsync(model, customerId);
|
||||
|
||||
return View("~/Plugins/Misc.FruitBankPlugin/Views/CustomerCreditWidget.cshtml", model);
|
||||
}
|
||||
|
||||
private async Task PopulatePlatesAsync(CustomerCreditWidgetModel model, int customerId)
|
||||
{
|
||||
var json = await _attributeService
|
||||
.GetGenericAttributeValueAsync<Customer, string>(customerId, FruitBankPluginConst.CustomerLicensePlatesAttribute, storeId: 0);
|
||||
|
||||
var entries = ParsePlates(json);
|
||||
model.LicensePlates = entries
|
||||
.Select(e => new CustomerLicensePlateRow { Plate = e.Plate, IsDefault = e.IsDefault })
|
||||
.ToList();
|
||||
model.PlateCount = model.LicensePlates.Count;
|
||||
model.DefaultPlate = model.LicensePlates.FirstOrDefault(p => p.IsDefault)?.Plate;
|
||||
}
|
||||
|
||||
private static List<CustomerLicensePlateEntry> ParsePlates(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return new();
|
||||
try { return JsonSerializer.Deserialize<List<CustomerLicensePlateEntry>>(json) ?? new(); }
|
||||
catch { return new(); }
|
||||
}
|
||||
|
||||
// Merge the customer's current addresses with the saved site list (orphaned
|
||||
// address ids — deleted addresses — are dropped because we iterate addresses).
|
||||
private async Task PopulateSitesAsync(CustomerCreditWidgetModel model, int customerId)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
// File: Plugins/YourCompany.ProductAttributes/Components/ProductAttributesViewComponent.cs
|
||||
|
||||
using System.Text.Json;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Nop.Core;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Customers;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Models;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Models.Orders;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
using Nop.Services.Common;
|
||||
using Nop.Services.Customers;
|
||||
using Nop.Web.Areas.Admin.Models.Catalog;
|
||||
using Nop.Web.Areas.Admin.Models.Orders;
|
||||
using Nop.Web.Framework.Components;
|
||||
|
|
@ -21,13 +24,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Components
|
|||
private readonly FruitBankAttributeService _fruitBankAttributeService;
|
||||
private readonly IWorkContext _workContext;
|
||||
private readonly IStoreContext _storeContext;
|
||||
private readonly ICustomerService _customerService;
|
||||
private FruitBankDbContext _dbContext;
|
||||
|
||||
public OrderAttributesViewComponent(FruitBankDbContext dbContext, FruitBankAttributeService fruitBankAttributeService, IWorkContext workContext, IStoreContext storeContext)
|
||||
public OrderAttributesViewComponent(FruitBankDbContext dbContext, FruitBankAttributeService fruitBankAttributeService,
|
||||
IWorkContext workContext, IStoreContext storeContext, ICustomerService customerService)
|
||||
{
|
||||
_workContext = workContext;
|
||||
_storeContext = storeContext;
|
||||
_fruitBankAttributeService = fruitBankAttributeService;
|
||||
_customerService = customerService;
|
||||
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
|
@ -45,19 +51,69 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Components
|
|||
model.IsMeasurable = orderDto.IsMeasurable;
|
||||
model.DateOfReceipt = orderDto.DateOfReceipt;
|
||||
model.OrderDto = orderDto;
|
||||
//var orderPickupAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Order, DateTime?>(model.OrderId, nameof(IOrderDto.DateOfReceipt));
|
||||
|
||||
//if (orderPickupAttributeValue.HasValue && orderPickupAttributeValue.Value != DateTime.MinValue)
|
||||
//{
|
||||
// model.DateOfReceipt = orderPickupAttributeValue;
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// model.DateOfReceipt = null;
|
||||
//}
|
||||
// Site (telephely) + license plate (rendszám)
|
||||
model.CustomerId = orderDto.CustomerId;
|
||||
model.Site = orderDto.Site;
|
||||
model.LicensePlate = orderDto.LicensePlate;
|
||||
model.CustomerSites = await BuildCustomerSiteOptionsAsync(orderDto.CustomerId);
|
||||
model.CustomerLicensePlateOptions = await BuildCustomerPlateOptionsAsync(orderDto.CustomerId);
|
||||
}
|
||||
|
||||
return View("~/Plugins/Misc.FruitBankPlugin/Views/OrderAttributes.cshtml", model);
|
||||
}
|
||||
|
||||
// The customer's current sites (telephely) → selector options. Orphaned site
|
||||
// ids (deleted addresses) drop out because we iterate the live addresses.
|
||||
private async Task<List<OrderSiteOption>> BuildCustomerSiteOptionsAsync(int customerId)
|
||||
{
|
||||
if (customerId <= 0) return new();
|
||||
|
||||
var json = await _fruitBankAttributeService
|
||||
.GetGenericAttributeValueAsync<Customer, string>(customerId, FruitBankPluginConst.CustomerSitesAttribute, 0);
|
||||
if (string.IsNullOrWhiteSpace(json)) return new();
|
||||
|
||||
List<CustomerSiteEntry> entries;
|
||||
try { entries = JsonSerializer.Deserialize<List<CustomerSiteEntry>>(json) ?? new(); }
|
||||
catch { return new(); }
|
||||
if (entries.Count == 0) return new();
|
||||
|
||||
var byId = entries.GroupBy(e => e.AddressId).ToDictionary(g => g.Key, g => g.First());
|
||||
var addresses = await _customerService.GetAddressesByCustomerIdAsync(customerId);
|
||||
|
||||
return addresses
|
||||
.Where(a => byId.ContainsKey(a.Id))
|
||||
.Select(a => new OrderSiteOption
|
||||
{
|
||||
AddressId = a.Id,
|
||||
Display = FormatAddress(a),
|
||||
IsDefault = byId[a.Id].IsDefault
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<string>> BuildCustomerPlateOptionsAsync(int customerId)
|
||||
{
|
||||
if (customerId <= 0) return new();
|
||||
|
||||
var json = await _fruitBankAttributeService
|
||||
.GetGenericAttributeValueAsync<Customer, string>(customerId, FruitBankPluginConst.CustomerLicensePlatesAttribute, 0);
|
||||
if (string.IsNullOrWhiteSpace(json)) return new();
|
||||
|
||||
try
|
||||
{
|
||||
var entries = JsonSerializer.Deserialize<List<CustomerLicensePlateEntry>>(json) ?? new();
|
||||
return entries.Select(e => e.Plate).Where(p => !string.IsNullOrWhiteSpace(p)).Distinct().ToList();
|
||||
}
|
||||
catch { return new(); }
|
||||
}
|
||||
|
||||
private static string FormatAddress(Address a)
|
||||
{
|
||||
var name = !string.IsNullOrWhiteSpace(a.Company) ? a.Company : $"{a.FirstName} {a.LastName}".Trim();
|
||||
var locality = $"{a.ZipPostalCode} {a.City}".Trim();
|
||||
var display = string.Join(", ", new[] { name, a.Address1, locality }.Where(p => !string.IsNullOrWhiteSpace(p)));
|
||||
return string.IsNullOrWhiteSpace(display) ? $"#{a.Id}" : display;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ using Nop.Services.Events;
|
|||
using Mango.Nop.Core.Extensions;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Services.Catalog;
|
||||
using Nop.Services.Customers;
|
||||
using Nop.Core.Domain.Customers;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Models;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.EventConsumers;
|
||||
|
||||
|
|
@ -31,7 +34,9 @@ public class FruitBankEventConsumer :
|
|||
|
||||
IConsumer<EntityInsertedEvent<OrderItem>>,
|
||||
IConsumer<EntityUpdatedEvent<OrderItem>>,
|
||||
IConsumer<EntityDeletedEvent<OrderItem>>
|
||||
IConsumer<EntityDeletedEvent<OrderItem>>,
|
||||
|
||||
IConsumer<OrderPlacedEvent>
|
||||
{
|
||||
//private readonly CustomPriceCalculationService _customPriceCalculationService;
|
||||
|
||||
|
|
@ -40,16 +45,100 @@ public class FruitBankEventConsumer :
|
|||
private readonly FruitBankAttributeService _fruitBankAttributeService;
|
||||
private readonly PreOrderConversionService _preorderConversionService;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ICustomerService _customerService;
|
||||
|
||||
public FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBankDbContext ctx, MeasurementService measurementService,
|
||||
FruitBankAttributeService fruitBankAttributeService, PreOrderConversionService preorderConversionService,
|
||||
IServiceScopeFactory serviceScopeFactory, IEnumerable<IAcLogWriterBase> logWriters) : base(ctx, httpContextAcc, logWriters)
|
||||
IServiceScopeFactory serviceScopeFactory, ICustomerService customerService, IEnumerable<IAcLogWriterBase> logWriters) : base(ctx, httpContextAcc, logWriters)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_measurementService = measurementService;
|
||||
_fruitBankAttributeService = fruitBankAttributeService;
|
||||
_preorderConversionService = preorderConversionService;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_customerService = customerService;
|
||||
}
|
||||
|
||||
// ── Order placed: auto-assign the customer's DEFAULT site (telephely) and DEFAULT
|
||||
// license plate (rendszám) to the order. Covers every creation path that fires
|
||||
// OrderPlacedEvent (checkout, admin, preorder conversion, OrderDraft). No default
|
||||
// → left empty. The two are independent (one missing must not skip the other).
|
||||
public async Task HandleEventAsync(OrderPlacedEvent eventMessage)
|
||||
{
|
||||
var order = eventMessage.Order;
|
||||
if (order == null || order.CustomerId <= 0) return;
|
||||
|
||||
await TryAssignDefaultSiteAsync(order);
|
||||
await TryAssignDefaultPlateAsync(order);
|
||||
}
|
||||
|
||||
private async Task TryAssignDefaultSiteAsync(Order order)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Don't overwrite an already-set site (e.g. event re-publish).
|
||||
var existing = await _fruitBankAttributeService
|
||||
.GetGenericAttributeValueAsync<Order, string>(order.Id, FruitBankPluginConst.OrderSiteAttribute, 0);
|
||||
if (!string.IsNullOrWhiteSpace(existing)) return;
|
||||
|
||||
var sitesJson = await _fruitBankAttributeService
|
||||
.GetGenericAttributeValueAsync<Customer, string>(order.CustomerId, FruitBankPluginConst.CustomerSitesAttribute, 0);
|
||||
if (string.IsNullOrWhiteSpace(sitesJson)) return;
|
||||
|
||||
List<CustomerSiteEntry> sites;
|
||||
try { sites = System.Text.Json.JsonSerializer.Deserialize<List<CustomerSiteEntry>>(sitesJson) ?? new(); }
|
||||
catch { return; }
|
||||
|
||||
var defaultSite = sites.FirstOrDefault(s => s.IsDefault);
|
||||
if (defaultSite == null) return;
|
||||
|
||||
var address = (await _customerService.GetAddressesByCustomerIdAsync(order.CustomerId))
|
||||
.FirstOrDefault(a => a.Id == defaultSite.AddressId);
|
||||
if (address == null) return;
|
||||
|
||||
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
|
||||
var snapshot = OrderSiteSnapshotBuilder.FromAddress(address, customer?.VatNumber);
|
||||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(snapshot);
|
||||
|
||||
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Order, string>(
|
||||
order.Id, FruitBankPluginConst.OrderSiteAttribute, json, 0);
|
||||
|
||||
Logger.Info($"[FruitBankEventConsumer] Order #{order.Id}: auto-assigned default site (address #{address.Id}).");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"[FruitBankEventConsumer] OrderPlaced site auto-assign failed for order #{order.Id}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryAssignDefaultPlateAsync(Order order)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await _fruitBankAttributeService
|
||||
.GetGenericAttributeValueAsync<Order, string>(order.Id, FruitBankPluginConst.OrderLicensePlateAttribute, 0);
|
||||
if (!string.IsNullOrWhiteSpace(existing)) return;
|
||||
|
||||
var platesJson = await _fruitBankAttributeService
|
||||
.GetGenericAttributeValueAsync<Customer, string>(order.CustomerId, FruitBankPluginConst.CustomerLicensePlatesAttribute, 0);
|
||||
if (string.IsNullOrWhiteSpace(platesJson)) return;
|
||||
|
||||
List<CustomerLicensePlateEntry> plates;
|
||||
try { plates = System.Text.Json.JsonSerializer.Deserialize<List<CustomerLicensePlateEntry>>(platesJson) ?? new(); }
|
||||
catch { return; }
|
||||
|
||||
var defaultPlate = plates.FirstOrDefault(p => p.IsDefault)?.Plate;
|
||||
if (string.IsNullOrWhiteSpace(defaultPlate)) return;
|
||||
|
||||
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Order, string>(
|
||||
order.Id, FruitBankPluginConst.OrderLicensePlateAttribute, defaultPlate, 0);
|
||||
|
||||
Logger.Info($"[FruitBankEventConsumer] Order #{order.Id}: auto-assigned default license plate '{defaultPlate}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"[FruitBankEventConsumer] OrderPlaced plate auto-assign failed for order #{order.Id}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task HandleEventAsync(EntityUpdatedEvent<Product> eventMessage)
|
||||
|
|
|
|||
|
|
@ -32,5 +32,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
|||
|
||||
/// <summary>JSON list of the customer's site (telephely) addresses — see CustomerSiteEntry.</summary>
|
||||
public const string CustomerSitesAttribute = "customerSites";
|
||||
|
||||
/// <summary>JSON list of the customer's selectable license plates — see CustomerLicensePlateEntry.</summary>
|
||||
public const string CustomerLicensePlatesAttribute = "customerLicensePlates";
|
||||
|
||||
// ── Order generic-attribute keys (KeyGroup "Order", StoreId 0) ───────────
|
||||
// MUST match the IOrderDto/OrderDto property names (the DTO reads them by nameof).
|
||||
/// <summary>JSON snapshot of the order's site (telephely) — see FruitBank.Common OrderSiteSnapshot.</summary>
|
||||
public const string OrderSiteAttribute = "Site";
|
||||
|
||||
/// <summary>License plate (rendszám) of the vehicle picking up the order. Raw; the NAV mapper normalizes.</summary>
|
||||
public const string OrderLicensePlateAttribute = "LicensePlate";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -378,6 +378,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
|||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerSites.NoAddresses", "This customer has no addresses yet (Addresses tab).", en);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerSites.NoAddresses", "Ehhez az ügyfélhez még nincs cím rögzítve a Címek fülön.", hu);
|
||||
|
||||
// ── Customer license plates (rendszámok) ────────────────────────────
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.Title", "License plates", en);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.Title", "Rendszámok", hu);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.Edit", "Edit plates", en);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.Edit", "Rendszámok szerkesztése", hu);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.CountWord", "plate(s)", en);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.CountWord", "rendszám", hu);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.None", "No plates set", en);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.None", "Nincs rendszám megadva", hu);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.ModalTitle", "Manage plates", en);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.ModalTitle", "Rendszámok kezelése", hu);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.Add", "Add", en);
|
||||
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerPlates.Add", "Hozzáadás", 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -118,4 +118,24 @@
|
|||
<Value><![CDATA[This customer has no addresses yet (Addresses tab).]]></Value>
|
||||
</LocaleResource>
|
||||
|
||||
<!-- License plates — Plugins.Misc.FruitBankPlugin.CustomerPlates.* -->
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.Title">
|
||||
<Value><![CDATA[License plates]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.Edit">
|
||||
<Value><![CDATA[Edit plates]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.CountWord">
|
||||
<Value><![CDATA[plate(s)]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.None">
|
||||
<Value><![CDATA[No plates set]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.ModalTitle">
|
||||
<Value><![CDATA[Manage plates]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.Add">
|
||||
<Value><![CDATA[Add]]></Value>
|
||||
</LocaleResource>
|
||||
|
||||
</Language>
|
||||
|
|
|
|||
|
|
@ -118,4 +118,24 @@
|
|||
<Value><![CDATA[Ehhez az ügyfélhez még nincs cím rögzítve a Címek fülön.]]></Value>
|
||||
</LocaleResource>
|
||||
|
||||
<!-- Rendszámok — Plugins.Misc.FruitBankPlugin.CustomerPlates.* -->
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.Title">
|
||||
<Value><![CDATA[Rendszámok]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.Edit">
|
||||
<Value><![CDATA[Rendszámok szerkesztése]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.CountWord">
|
||||
<Value><![CDATA[rendszám]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.None">
|
||||
<Value><![CDATA[Nincs rendszám megadva]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.ModalTitle">
|
||||
<Value><![CDATA[Rendszámok kezelése]]></Value>
|
||||
</LocaleResource>
|
||||
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.CustomerPlates.Add">
|
||||
<Value><![CDATA[Hozzáadás]]></Value>
|
||||
</LocaleResource>
|
||||
|
||||
</Language>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@ public record CustomerCreditWidgetModel : BaseNopModel
|
|||
|
||||
/// <summary>Display string of the default site, if any.</summary>
|
||||
public string? DefaultSiteDisplay { get; set; }
|
||||
|
||||
// ── License plates (rendszámok) ────────────────────────────────────────────
|
||||
/// <summary>The customer's selectable plates — feeds the summary and the edit modal.</summary>
|
||||
public List<CustomerLicensePlateRow> LicensePlates { get; set; } = new();
|
||||
|
||||
public int PlateCount { get; set; }
|
||||
|
||||
/// <summary>The default plate, if any (auto-assigned to new orders).</summary>
|
||||
public string? DefaultPlate { get; set; }
|
||||
}
|
||||
|
||||
public class CustomerSiteAddressRow
|
||||
|
|
@ -41,3 +50,9 @@ public class CustomerSiteAddressRow
|
|||
public bool IsSite { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
}
|
||||
|
||||
public class CustomerLicensePlateRow
|
||||
{
|
||||
public string Plate { get; set; } = string.Empty;
|
||||
public bool IsDefault { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Models;
|
||||
|
||||
/// <summary>
|
||||
/// One selectable license plate (rendszám) in a customer's plate list, stored as JSON
|
||||
/// in the <c>customerLicensePlates</c> generic attribute. Extensible with per-plate
|
||||
/// fields (e.g. Note) later. One entry may be the default, auto-assigned to new orders.
|
||||
/// </summary>
|
||||
public class CustomerLicensePlateEntry
|
||||
{
|
||||
[JsonPropertyName("plate")]
|
||||
public string Plate { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("isDefault")]
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
[JsonPropertyName("note")]
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
|
@ -17,5 +17,25 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Orders
|
|||
|
||||
public OrderDto OrderDto { get; set; }
|
||||
|
||||
// ── Site (telephely) + license plate (rendszám) ────────────────────────
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
/// <summary>The order's current frozen site snapshot, if any.</summary>
|
||||
public OrderSiteSnapshot? Site { get; set; }
|
||||
|
||||
public string? LicensePlate { get; set; }
|
||||
|
||||
/// <summary>The customer's current sites — choices for the order-site selector.</summary>
|
||||
public List<OrderSiteOption> CustomerSites { get; set; } = new();
|
||||
|
||||
/// <summary>The customer's saved license plates — suggestions (datalist) for the order plate field.</summary>
|
||||
public List<string> CustomerLicensePlateOptions { get; set; } = new();
|
||||
}
|
||||
|
||||
public class OrderSiteOption
|
||||
{
|
||||
public int AddressId { get; set; }
|
||||
public string Display { get; set; } = string.Empty;
|
||||
public bool IsDefault { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
using FruitBank.Common.Dtos;
|
||||
using Nop.Core.Domain.Common;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the frozen <see cref="OrderSiteSnapshot"/> for an order from a customer
|
||||
/// Address. Shaped to the NAV EKÁER LocationType (UnloadLocation). Stateless — used
|
||||
/// by both the OrderPlacedEvent auto-assign and the manual save endpoint.
|
||||
/// </summary>
|
||||
public static class OrderSiteSnapshotBuilder
|
||||
{
|
||||
public static OrderSiteSnapshot FromAddress(Address a, string? customerVatNumber)
|
||||
{
|
||||
var name = !string.IsNullOrWhiteSpace(a.Company)
|
||||
? a.Company
|
||||
: $"{a.FirstName} {a.LastName}".Trim();
|
||||
|
||||
var street = string.Join(" ", new[] { a.Address1, a.Address2 }
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
|
||||
var locality = $"{a.ZipPostalCode} {a.City}".Trim();
|
||||
|
||||
var display = string.Join(", ", new[] { name, street, locality }
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
|
||||
return new OrderSiteSnapshot
|
||||
{
|
||||
SourceAddressId = a.Id,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
VatNumber = customerVatNumber,
|
||||
CountryCode = "HU", // outgoing EKÁER is domestic; export would resolve a.CountryId
|
||||
ZipCode = a.ZipPostalCode,
|
||||
City = a.City,
|
||||
Street = string.IsNullOrWhiteSpace(street) ? null : street,
|
||||
Display = string.IsNullOrWhiteSpace(display) ? $"#{a.Id}" : display
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +78,32 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-3">
|
||||
<label>@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.Title")</label>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<span class="form-control-plaintext" id="fruitbank-plates-summary">
|
||||
@if (Model.PlateCount > 0)
|
||||
{
|
||||
<text>@Model.PlateCount @T("Plugins.Misc.FruitBankPlugin.CustomerPlates.CountWord")</text>
|
||||
if (!string.IsNullOrWhiteSpace(Model.DefaultPlate))
|
||||
{
|
||||
<text> (@T("Plugins.Misc.FruitBankPlugin.CustomerSites.DefaultLabel"): @Model.DefaultPlate)</text>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.None")</span>
|
||||
}
|
||||
</span>
|
||||
<button type="button" class="btn btn-default mt-1" data-toggle="modal" data-target="#fruitbank-plates-modal">
|
||||
<i class="fas fa-car"></i>
|
||||
@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.Edit")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -218,6 +244,51 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@* ── License plates edit modal ─────────────────────────────────────────────── *@
|
||||
<div id="fruitbank-plates-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="fruitbank-plates-modal-title">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="fruitbank-plates-modal-title">@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.ModalTitle")</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" id="fb-plate-new" class="form-control" placeholder="pl. ABC-123" />
|
||||
<div class="input-group-append">
|
||||
<button type="button" id="fb-plate-add" class="btn btn-secondary">@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.Add")</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-sm table-hover" id="fb-plate-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:120px" class="text-center">@T("Plugins.Misc.FruitBankPlugin.CustomerSites.DefaultLabel")</th>
|
||||
<th>@T("Plugins.Misc.FruitBankPlugin.CustomerPlates.Title")</th>
|
||||
<th style="width:50px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fb-plate-tbody">
|
||||
@foreach (var p in Model.LicensePlates)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-center"><input type="radio" name="fb-plate-default" value="@p.Plate" @(p.IsDefault ? "checked" : "") /></td>
|
||||
<td><span class="fb-plate-text">@p.Plate</span></td>
|
||||
<td class="text-center"><button type="button" class="btn btn-sm btn-link text-danger fb-plate-remove" title="Törlés">×</button></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">@T("Admin.Common.Cancel")</button>
|
||||
<button type="button" class="btn btn-primary" id="fruitbank-plates-save" data-customer-id="@Model.CustomerId">
|
||||
@T("Plugins.Misc.FruitBankPlugin.CustomerCredit.Save")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// The widget is rendered into the CustomerDetailsBlock zone at the bottom of
|
||||
|
|
@ -330,5 +401,62 @@
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── License plates edit modal ──────────────────────────────────────────
|
||||
function fbNormalizePlate(v) { return (v || '').trim().toUpperCase(); }
|
||||
|
||||
function fbAddPlateRow(plate, isDefault) {
|
||||
var $tbody = $('#fb-plate-tbody');
|
||||
var exists = false;
|
||||
$tbody.find('.fb-plate-text').each(function () { if ($(this).text() === plate) exists = true; });
|
||||
if (exists) return;
|
||||
var row = '<tr>' +
|
||||
'<td class="text-center"><input type="radio" name="fb-plate-default" value="' + plate + '"' + (isDefault ? ' checked' : '') + ' /></td>' +
|
||||
'<td><span class="fb-plate-text">' + plate + '</span></td>' +
|
||||
'<td class="text-center"><button type="button" class="btn btn-sm btn-link text-danger fb-plate-remove" title="Törlés">×</button></td>' +
|
||||
'</tr>';
|
||||
$tbody.append(row);
|
||||
}
|
||||
|
||||
$('#fb-plate-add').on('click', function () {
|
||||
var plate = fbNormalizePlate($('#fb-plate-new').val());
|
||||
if (!plate) return;
|
||||
fbAddPlateRow(plate, false);
|
||||
$('#fb-plate-new').val('').focus();
|
||||
});
|
||||
|
||||
$('#fb-plate-new').on('keypress', function (e) {
|
||||
if (e.which === 13) { e.preventDefault(); $('#fb-plate-add').trigger('click'); }
|
||||
});
|
||||
|
||||
$('#fruitbank-plates-modal').on('click', '.fb-plate-remove', function () {
|
||||
$(this).closest('tr').remove();
|
||||
});
|
||||
|
||||
$('#fruitbank-plates-save').on('click', function () {
|
||||
var $btn = $(this);
|
||||
var plates = [];
|
||||
$('#fb-plate-tbody .fb-plate-text').each(function () { plates.push($(this).text()); });
|
||||
var def = $('#fruitbank-plates-modal input[name="fb-plate-default"]:checked').val() || '';
|
||||
var token = $('input[name="__RequestVerificationToken"]').first().val();
|
||||
|
||||
$btn.prop('disabled', true);
|
||||
$.ajax({
|
||||
url: '@Url.Action("SaveLicensePlates", "CustomerAttributes")',
|
||||
type: 'POST',
|
||||
traditional: true, // serialize plates as repeated keys for string[] binding
|
||||
data: {
|
||||
customerId: $btn.data('customer-id'),
|
||||
plates: plates,
|
||||
defaultPlate: def,
|
||||
__RequestVerificationToken: token
|
||||
},
|
||||
success: function (res) {
|
||||
if (res && res.success) { location.reload(); }
|
||||
else { alert(res && res.error ? res.error : 'Hiba a mentés során.'); $btn.prop('disabled', false); }
|
||||
},
|
||||
error: function () { alert('Hiba a mentés során.'); $btn.prop('disabled', false); }
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
@using Nop.Plugin.Misc.FruitBankPlugin.Models.Orders
|
||||
@using System.Linq
|
||||
@using Nop.Plugin.Misc.FruitBankPlugin.Models.Orders
|
||||
@model OrderAttributesModel
|
||||
|
||||
|
||||
|
|
@ -37,6 +38,69 @@
|
|||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
@* ── Telephely ─────────────────────────────────────────────── *@
|
||||
<div class="form-group row">
|
||||
<div class="col-md-3">
|
||||
<label>Telephely</label>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@if (Model.CustomerSites.Any())
|
||||
{
|
||||
<select id="orderSiteSelect" class="form-control" data-current="@(Model.Site?.SourceAddressId ?? 0)">
|
||||
<option value="0">— nincs —</option>
|
||||
@foreach (var s in Model.CustomerSites)
|
||||
{
|
||||
<option value="@s.AddressId">@s.Display @(s.IsDefault ? "(alapértelmezett)" : "")</option>
|
||||
}
|
||||
</select>
|
||||
@if (Model.Site != null && Model.CustomerSites.All(s => s.AddressId != Model.Site.SourceAddressId))
|
||||
{
|
||||
<small class="text-muted">Jelenlegi (már nem a vevő telephelye): @Model.Site.Display</small>
|
||||
}
|
||||
else if (Model.Site == null)
|
||||
{
|
||||
<small class="text-warning"><i class="fas fa-exclamation-circle"></i> Nincs telephely beállítva ehhez a rendeléshez.</small>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning mb-0 py-1 px-2">
|
||||
<i class="fas fa-exclamation-triangle"></i> A vevőnek nincs telephelye beállítva.
|
||||
<a href="/Admin/Customer/Edit/@Model.CustomerId">Beállítás a vevőnél</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-3 text-right">
|
||||
@if (Model.CustomerSites.Any())
|
||||
{
|
||||
<button type="button" id="saveOrderSiteBtn" class="btn btn-primary">
|
||||
<i class="fa fa-save"></i> Mentés
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
@* ── Rendszám ──────────────────────────────────────────────── *@
|
||||
<div class="form-group row">
|
||||
<div class="col-md-3">
|
||||
<label>Rendszám</label>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="text" id="orderLicensePlate" class="form-control" value="@Model.LicensePlate" placeholder="pl. ABC-123" list="orderPlateList" autocomplete="off" />
|
||||
<datalist id="orderPlateList">
|
||||
@foreach (var p in Model.CustomerLicensePlateOptions)
|
||||
{
|
||||
<option value="@p"></option>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="col-md-3 text-right">
|
||||
<button type="button" id="saveLicensePlateBtn" class="btn btn-primary">
|
||||
<i class="fa fa-save"></i> Mentés
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="form-group row">
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-warning btn-block" data-toggle="modal" data-target="#allowRevisionModal">
|
||||
|
|
@ -1336,3 +1400,48 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@* ── Site (telephely) + license plate (rendszám) save ───────────────────────── *@
|
||||
<script>
|
||||
$(function () {
|
||||
// Preselect the order's current site (avoids C# in the <option> attribute area)
|
||||
var $siteSel = $('#orderSiteSelect');
|
||||
if ($siteSel.length) { $siteSel.val(String($siteSel.data('current'))); }
|
||||
|
||||
$('#saveOrderSiteBtn').on('click', function () {
|
||||
var $btn = $(this).prop('disabled', true);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/Admin/OrderAttributes/SaveSite',
|
||||
data: {
|
||||
orderId: @Model.OrderId,
|
||||
addressId: $('#orderSiteSelect').val(),
|
||||
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
|
||||
},
|
||||
success: function (r) {
|
||||
if (r && r.success) { location.reload(); }
|
||||
else { alert((r && r.error) || 'Hiba a mentés során.'); $btn.prop('disabled', false); }
|
||||
},
|
||||
error: function () { alert('Hiba a mentés során.'); $btn.prop('disabled', false); }
|
||||
});
|
||||
});
|
||||
|
||||
$('#saveLicensePlateBtn').on('click', function () {
|
||||
var $btn = $(this).prop('disabled', true);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/Admin/OrderAttributes/SaveLicensePlate',
|
||||
data: {
|
||||
orderId: @Model.OrderId,
|
||||
licensePlate: $('#orderLicensePlate').val(),
|
||||
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
|
||||
},
|
||||
success: function (r) {
|
||||
if (r && r.success) { $('#orderLicensePlate').val(r.licensePlate || ''); $btn.prop('disabled', false); }
|
||||
else { alert((r && r.error) || 'Hiba a mentés során.'); $btn.prop('disabled', false); }
|
||||
},
|
||||
error: function () { alert('Hiba a mentés során.'); $btn.prop('disabled', false); }
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# InnVoice integration — current state & planned migration
|
||||
|
||||
> Reference doc for `Nop.Plugin.Misc.FruitBankPlugin`. See [`README.md`](README.md) for the docs index.
|
||||
> Scope: how the plugin pushes orders/invoices to the external **InnVoice** system from the admin order page.
|
||||
|
||||
## Where it lives
|
||||
|
||||
The admin **order edit** widget ([`Views/OrderAttributes.cshtml`](../Views/OrderAttributes.cshtml), rendered by `OrderAttributesViewComponent` into `AdminWidgetZones.OrderDetailsBlock`) hosts the InnVoice UI in its right-hand card *"Megrendelés beküldése Innvoice-ba"*.
|
||||
|
||||
Related server side: `InnVoiceApiService` (`Services/`), `InnVoiceOrderController` and `InvoiceController` (`Areas/Admin/Controllers/`).
|
||||
|
||||
## Current behaviour (order submission)
|
||||
|
||||
The active flow submits an **order** to InnVoice:
|
||||
|
||||
| UI | Endpoint |
|
||||
|---|---|
|
||||
| "Megrendelés beküldése" | `InnVoiceOrder/CreateOrder` |
|
||||
| "Invoice adat ellenőrzése" | `InnVoiceOrder/GetOrderStatus` (also auto-run on page load) |
|
||||
|
||||
On success the card shows the InnVoice Order Table ID / Tech ID and a PDF link.
|
||||
|
||||
## Orphaned invoice JS (intentional, kept for the migration)
|
||||
|
||||
`OrderAttributes.cshtml` also contains a **full invoice-handling JS block** — handlers for `#createInvoiceBtn` / `#checkInvoiceBtn`, `displayInvoiceDetails(...)`, calls to `Invoice/CreateInvoice` and `Invoice/GetInvoiceStatus`, plus a page-load `GetInvoiceStatus` AJAX.
|
||||
|
||||
**These have no matching markup** (`#createInvoiceBtn`, `#invoiceDetails`, `#invoiceNumber`, `#invoicePdfLink`, … do not exist in the view), so the code is currently dead — the only live side effect is a silent, failing `GetInvoiceStatus` call on every order page load.
|
||||
|
||||
This is **not a bug to clean up yet**: it is groundwork for the planned migration below.
|
||||
|
||||
## Planned migration — direct invoice submission (draft)
|
||||
|
||||
We will switch the InnVoice integration from the current **order submission** to **direct invoice submission**, creating the invoice in **draft ("piszkozat") status** in InnVoice.
|
||||
|
||||
When we return to this:
|
||||
- Wire the existing invoice JS to real buttons/markup in the InnVoice card (replacing or complementing the current order-submission buttons).
|
||||
- Confirm `Invoice/CreateInvoice` creates the invoice in draft status.
|
||||
- Decide the fate of the current `InnVoiceOrder/CreateOrder` order-submission path (keep, deprecate, or remove).
|
||||
- Remove the wasted page-load `GetInvoiceStatus` call if it is not needed in the final design.
|
||||
|
||||
_Status: planned, not started. Recorded so the orphaned invoice code is understood as intentional._
|
||||
|
|
@ -9,6 +9,7 @@ Topic documentation for the FruitBank-specific NopCommerce plugin (Layer 3 — c
|
|||
- [`SCHEMA.md`](SCHEMA.md) — Database schema (Toon format — LLM-optimized)
|
||||
- [`DOMAIN_MODEL.md`](DOMAIN_MODEL.md) — Business domain behavior
|
||||
- [`MEASUREMENT.md`](MEASUREMENT.md) — Weight measurement logic (NetWeight formula, MeasuringStatus lifecycle)
|
||||
- [`INNVOICE.md`](INNVOICE.md) — InnVoice integration: current order-submission flow, the orphaned invoice JS, and the planned migration to direct draft-invoice submission
|
||||
|
||||
## Topic folders
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue