diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomerAttributesController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomerAttributesController.cs index 95d06bd..2ac7cab 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomerAttributesController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomerAttributesController.cs @@ -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 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()) + .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( + customerId, FruitBankPluginConst.CustomerLicensePlatesAttribute, json, storeId: 0); + + return Json(new { success = true, plateCount = entries.Count, defaultPlate = effectiveDefault }); + } } diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/OrderAttributesController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/OrderAttributesController.cs new file mode 100644 index 0000000..0cc8cc6 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/OrderAttributesController.cs @@ -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; + +/// +/// 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 , +/// using the same keys the OrderDto exposes. +/// +[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 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(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(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( + orderId, FruitBankPluginConst.OrderSiteAttribute, json, 0); + + return Json(new { success = true, display = snapshot.Display }); + } + + [HttpPost] + [Route("Admin/OrderAttributes/SaveLicensePlate")] + public async Task 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(orderId, FruitBankPluginConst.OrderLicensePlateAttribute, 0); + return Json(new { success = true, cleared = true }); + } + + await _attributeService.InsertOrUpdateGenericAttributeAsync( + orderId, FruitBankPluginConst.OrderLicensePlateAttribute, plate, 0); + + return Json(new { success = true, licensePlate = plate }); + } + + private static HashSet ParseSiteAddressIds(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return new(); + try + { + var entries = JsonSerializer.Deserialize>(json) ?? new(); + return entries.Select(e => e.AddressId).ToHashSet(); + } + catch { return new(); } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Components/CustomerCreditWidgetViewComponent.cs b/Nop.Plugin.Misc.AIPlugin/Components/CustomerCreditWidgetViewComponent.cs index 3fb892d..0cafbfa 100644 --- a/Nop.Plugin.Misc.AIPlugin/Components/CustomerCreditWidgetViewComponent.cs +++ b/Nop.Plugin.Misc.AIPlugin/Components/CustomerCreditWidgetViewComponent.cs @@ -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(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 ParsePlates(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return new(); + try { return JsonSerializer.Deserialize>(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) diff --git a/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs b/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs index a832c88..3f92e14 100644 --- a/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs +++ b/Nop.Plugin.Misc.AIPlugin/Components/OrderAttributesViewComponent.cs @@ -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,15 +24,18 @@ 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; + _dbContext = dbContext; } public async Task InvokeAsync(string widgetZone, object additionalData) @@ -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(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> BuildCustomerSiteOptionsAsync(int customerId) + { + if (customerId <= 0) return new(); + + var json = await _fruitBankAttributeService + .GetGenericAttributeValueAsync(customerId, FruitBankPluginConst.CustomerSitesAttribute, 0); + if (string.IsNullOrWhiteSpace(json)) return new(); + + List entries; + try { entries = JsonSerializer.Deserialize>(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> BuildCustomerPlateOptionsAsync(int customerId) + { + if (customerId <= 0) return new(); + + var json = await _fruitBankAttributeService + .GetGenericAttributeValueAsync(customerId, FruitBankPluginConst.CustomerLicensePlatesAttribute, 0); + if (string.IsNullOrWhiteSpace(json)) return new(); + + try + { + var entries = JsonSerializer.Deserialize>(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; + } } } diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs index 2a23237..cb3611e 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs @@ -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>, IConsumer>, - IConsumer> + IConsumer>, + + IConsumer { //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 logWriters) : base(ctx, httpContextAcc, logWriters) + IServiceScopeFactory serviceScopeFactory, ICustomerService customerService, IEnumerable 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.Id, FruitBankPluginConst.OrderSiteAttribute, 0); + if (!string.IsNullOrWhiteSpace(existing)) return; + + var sitesJson = await _fruitBankAttributeService + .GetGenericAttributeValueAsync(order.CustomerId, FruitBankPluginConst.CustomerSitesAttribute, 0); + if (string.IsNullOrWhiteSpace(sitesJson)) return; + + List sites; + try { sites = System.Text.Json.JsonSerializer.Deserialize>(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.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.Id, FruitBankPluginConst.OrderLicensePlateAttribute, 0); + if (!string.IsNullOrWhiteSpace(existing)) return; + + var platesJson = await _fruitBankAttributeService + .GetGenericAttributeValueAsync(order.CustomerId, FruitBankPluginConst.CustomerLicensePlatesAttribute, 0); + if (string.IsNullOrWhiteSpace(platesJson)) return; + + List plates; + try { plates = System.Text.Json.JsonSerializer.Deserialize>(platesJson) ?? new(); } + catch { return; } + + var defaultPlate = plates.FirstOrDefault(p => p.IsDefault)?.Plate; + if (string.IsNullOrWhiteSpace(defaultPlate)) return; + + await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync( + 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 eventMessage) diff --git a/Nop.Plugin.Misc.AIPlugin/FruitBankConst.cs b/Nop.Plugin.Misc.AIPlugin/FruitBankConst.cs index e70f701..a54496e 100644 --- a/Nop.Plugin.Misc.AIPlugin/FruitBankConst.cs +++ b/Nop.Plugin.Misc.AIPlugin/FruitBankConst.cs @@ -32,5 +32,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin /// JSON list of the customer's site (telephely) addresses — see CustomerSiteEntry. public const string CustomerSitesAttribute = "customerSites"; + + /// JSON list of the customer's selectable license plates — see CustomerLicensePlateEntry. + 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). + /// JSON snapshot of the order's site (telephely) — see FruitBank.Common OrderSiteSnapshot. + public const string OrderSiteAttribute = "Site"; + + /// License plate (rendszám) of the vehicle picking up the order. Raw; the NAV mapper normalizes. + public const string OrderLicensePlateAttribute = "LicensePlate"; } } diff --git a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs index abe0783..dd03d9c 100644 --- a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs +++ b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs @@ -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); diff --git a/Nop.Plugin.Misc.AIPlugin/Localization/customercredit.en.xml b/Nop.Plugin.Misc.AIPlugin/Localization/customercredit.en.xml index 515bf8f..c72c0f6 100644 --- a/Nop.Plugin.Misc.AIPlugin/Localization/customercredit.en.xml +++ b/Nop.Plugin.Misc.AIPlugin/Localization/customercredit.en.xml @@ -118,4 +118,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Nop.Plugin.Misc.AIPlugin/Localization/customercredit.hu.xml b/Nop.Plugin.Misc.AIPlugin/Localization/customercredit.hu.xml index 260d54f..6991747 100644 --- a/Nop.Plugin.Misc.AIPlugin/Localization/customercredit.hu.xml +++ b/Nop.Plugin.Misc.AIPlugin/Localization/customercredit.hu.xml @@ -118,4 +118,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Nop.Plugin.Misc.AIPlugin/Models/CustomerCreditWidgetModel.cs b/Nop.Plugin.Misc.AIPlugin/Models/CustomerCreditWidgetModel.cs index b5adbb9..7ede298 100644 --- a/Nop.Plugin.Misc.AIPlugin/Models/CustomerCreditWidgetModel.cs +++ b/Nop.Plugin.Misc.AIPlugin/Models/CustomerCreditWidgetModel.cs @@ -32,6 +32,15 @@ public record CustomerCreditWidgetModel : BaseNopModel /// Display string of the default site, if any. public string? DefaultSiteDisplay { get; set; } + + // ── License plates (rendszámok) ──────────────────────────────────────────── + /// The customer's selectable plates — feeds the summary and the edit modal. + public List LicensePlates { get; set; } = new(); + + public int PlateCount { get; set; } + + /// The default plate, if any (auto-assigned to new orders). + 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; } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Models/CustomerLicensePlateEntry.cs b/Nop.Plugin.Misc.AIPlugin/Models/CustomerLicensePlateEntry.cs new file mode 100644 index 0000000..983d077 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Models/CustomerLicensePlateEntry.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Models; + +/// +/// One selectable license plate (rendszám) in a customer's plate list, stored as JSON +/// in the customerLicensePlates generic attribute. Extensible with per-plate +/// fields (e.g. Note) later. One entry may be the default, auto-assigned to new orders. +/// +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; } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Models/Orders/OrderAttributesModel.cs b/Nop.Plugin.Misc.AIPlugin/Models/Orders/OrderAttributesModel.cs index 49cf5cc..b86f348 100644 --- a/Nop.Plugin.Misc.AIPlugin/Models/Orders/OrderAttributesModel.cs +++ b/Nop.Plugin.Misc.AIPlugin/Models/Orders/OrderAttributesModel.cs @@ -14,8 +14,28 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Orders [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.DateOfReceipt")] public DateTime? DateOfReceipt { get; set; } - + public OrderDto OrderDto { get; set; } + // ── Site (telephely) + license plate (rendszám) ──────────────────────── + public int CustomerId { get; set; } + + /// The order's current frozen site snapshot, if any. + public OrderSiteSnapshot? Site { get; set; } + + public string? LicensePlate { get; set; } + + /// The customer's current sites — choices for the order-site selector. + public List CustomerSites { get; set; } = new(); + + /// The customer's saved license plates — suggestions (datalist) for the order plate field. + public List CustomerLicensePlateOptions { get; set; } = new(); + } + + public class OrderSiteOption + { + public int AddressId { get; set; } + public string Display { get; set; } = string.Empty; + public bool IsDefault { get; set; } } } \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Services/OrderSiteSnapshotBuilder.cs b/Nop.Plugin.Misc.AIPlugin/Services/OrderSiteSnapshotBuilder.cs new file mode 100644 index 0000000..136f144 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Services/OrderSiteSnapshotBuilder.cs @@ -0,0 +1,39 @@ +using FruitBank.Common.Dtos; +using Nop.Core.Domain.Common; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Services; + +/// +/// Builds the frozen 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. +/// +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 + }; + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Views/CustomerCreditWidget.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/CustomerCreditWidget.cshtml index 73b5961..39fefa1 100644 --- a/Nop.Plugin.Misc.AIPlugin/Views/CustomerCreditWidget.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Views/CustomerCreditWidget.cshtml @@ -78,6 +78,32 @@ + +
+
+ +
+
+ + @if (Model.PlateCount > 0) + { + @Model.PlateCount @T("Plugins.Misc.FruitBankPlugin.CustomerPlates.CountWord") + if (!string.IsNullOrWhiteSpace(Model.DefaultPlate)) + { + (@T("Plugins.Misc.FruitBankPlugin.CustomerSites.DefaultLabel"): @Model.DefaultPlate) + } + } + else + { + @T("Plugins.Misc.FruitBankPlugin.CustomerPlates.None") + } + + +
+
@@ -218,6 +244,51 @@ +@* ── License plates edit modal ─────────────────────────────────────────────── *@ + + diff --git a/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml index 36b8caf..a64d270 100644 --- a/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Views/OrderAttributes.cshtml @@ -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 @@
+ @* ── Telephely ─────────────────────────────────────────────── *@ +
+
+ +
+
+ @if (Model.CustomerSites.Any()) + { + + @if (Model.Site != null && Model.CustomerSites.All(s => s.AddressId != Model.Site.SourceAddressId)) + { + Jelenlegi (már nem a vevő telephelye): @Model.Site.Display + } + else if (Model.Site == null) + { + Nincs telephely beállítva ehhez a rendeléshez. + } + } + else + { +
+ A vevőnek nincs telephelye beállítva. + Beállítás a vevőnél +
+ } +
+
+ @if (Model.CustomerSites.Any()) + { + + } +
+
+
+ @* ── Rendszám ──────────────────────────────────────────────── *@ +
+
+ +
+
+ + + @foreach (var p in Model.CustomerLicensePlateOptions) + { + + } + +
+
+ +
+
+