Create Order

This commit is contained in:
Adam 2025-10-15 15:26:52 +02:00
parent 959cbf5d62
commit d87823bb41
7 changed files with 298 additions and 20 deletions

View File

@ -5,10 +5,14 @@ using FruitBank.Common.Server.Interfaces;
using FruitBank.Common.SignalRs; using FruitBank.Common.SignalRs;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Nop.Core.Domain.Orders; using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using Nop.Core.Domain.Shipping;
using Nop.Core.Domain.Tax;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Plugin.Misc.FruitBankPlugin.Factories; using Nop.Plugin.Misc.FruitBankPlugin.Factories;
using Nop.Plugin.Misc.FruitBankPlugin.Models; using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Services.Common; using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Messages; using Nop.Services.Messages;
using Nop.Services.Orders; using Nop.Services.Orders;
using Nop.Services.Security; using Nop.Services.Security;
@ -30,9 +34,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
private readonly IPermissionService _permissionService; private readonly IPermissionService _permissionService;
private readonly IGenericAttributeService _genericAttributeService; private readonly IGenericAttributeService _genericAttributeService;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly ICustomerService _customerService;
// ... other dependencies // ... other dependencies
public CustomOrderController(IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService, IGenericAttributeService genericAttributeService, INotificationService notificationService) public CustomOrderController(IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService, IGenericAttributeService genericAttributeService, INotificationService notificationService, ICustomerService customerService)
{ {
_orderService = orderService; _orderService = orderService;
_orderModelFactory = orderModelFactory as CustomOrderModelFactory; _orderModelFactory = orderModelFactory as CustomOrderModelFactory;
@ -40,6 +45,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
_permissionService = permissionService; _permissionService = permissionService;
_genericAttributeService = genericAttributeService; _genericAttributeService = genericAttributeService;
_notificationService = notificationService; _notificationService = notificationService;
_customerService = customerService;
// ... initialize other deps // ... initialize other deps
} }
@ -131,6 +137,93 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
return RedirectToAction("Edit", "Order", new { id = model.OrderId }); return RedirectToAction("Edit", "Order", new { id = model.OrderId });
} }
[HttpPost]
public virtual async Task<IActionResult> Create(int customerId)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE))
return AccessDeniedView();
// Validate customer exists
var customer = await _customerService.GetCustomerByIdAsync(customerId);
if (customer == null)
return RedirectToAction("List");
// Create new empty order
var order = new Order
{
OrderGuid = Guid.NewGuid(),
CustomerId = customerId,
CustomerLanguageId = customer.LanguageId ?? 1,
CustomerTaxDisplayType = (TaxDisplayType)customer.TaxDisplayType,
CustomerIp = string.Empty,
OrderStatusId = (int)OrderStatus.Pending,
PaymentStatusId = (int)PaymentStatus.Pending,
ShippingStatusId = (int)ShippingStatus.ShippingNotRequired,
CreatedOnUtc = DateTime.UtcNow,
BillingAddressId = customer.BillingAddressId ?? 0,
ShippingAddressId = customer.ShippingAddressId
};
await _orderService.InsertOrderAsync(order);
// Redirect to edit page
return RedirectToAction("Edit", new { id = order.Id });
}
[HttpGet] // Change from [HttpPost] to [HttpGet]
[CheckPermission(StandardPermission.Customers.CUSTOMERS_VIEW)]
public virtual async Task<IActionResult> CustomerSearchAutoComplete(string term)
{
if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
return Json(new List<object>());
const int maxResults = 15;
// Search by email (contains)
var customersByEmail = await _customerService.GetAllCustomersAsync(
email: term,
pageIndex: 0,
pageSize: maxResults);
// Search by first name (contains)
var customersByFirstName = await _customerService.GetAllCustomersAsync(
firstName: term,
pageIndex: 0,
pageSize: maxResults);
// Search by last name (contains)
var customersByLastName = await _customerService.GetAllCustomersAsync(
lastName: term,
pageIndex: 0,
pageSize: maxResults);
// Combine and deduplicate results
var allCustomers = customersByEmail
.Union(customersByFirstName)
.Union(customersByLastName)
.DistinctBy(c => c.Id)
.Take(maxResults)
.ToList();
var result = new List<object>();
foreach (var customer in allCustomers)
{
var fullName = await _customerService.GetCustomerFullNameAsync(customer);
var displayText = !string.IsNullOrEmpty(customer.Email)
? $"{customer.Email} ({fullName})"
: fullName;
result.Add(new
{
label = displayText,
value = customer.Id
});
}
return Json(result);
}
} }
} }

View File

@ -186,6 +186,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
return Json(model); return Json(model);
} }
[HttpGet]
public async Task<IActionResult> GetAllPartners()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
// Mock data for now
var model = await _dbContext.Partners.GetAll().ToListAsync();
var valami = model;
//model. = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId);
return Json(model);
}
[HttpPost] [HttpPost]
[RequestSizeLimit(10485760)] // 10MB [RequestSizeLimit(10485760)] // 10MB
@ -286,14 +300,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{ {
try try
{ {
// Open the PDF from the IFormFile's stream directly in memory // Open the Image from the IFormFile's stream directly in memory
using (var stream = file.OpenReadStream()) using (var stream = file.OpenReadStream())
{ {
try try
{ {
// ✅ Use the service we implemented earlier // ✅ Use the service we implemented earlier
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this PDF."); pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this image.");
} }
catch (Exception aiEx) catch (Exception aiEx)
{ {
@ -319,7 +333,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
} }
string analysisPrompt = "Extract the document identification number from this document, determine the type of the " + string analysisPrompt = "Extract the document identification number from this document, determine the type of the " +
"document IN ENGLISH from the available list, and return them as JSON: documentNumber, documentType. " + "document IN ENGLISH from the available list, and return them as JSON: documentNumber, documentType. " +
$"Available filetypes: {nameof(DocumentType.Invoice)}, {nameof(DocumentType.ShippingDocument)} , {nameof(DocumentType.OrderConfirmation)}, {nameof(DocumentType.Unknown)}" + $"Available filetypes: {nameof(DocumentType.Invoice)}, {nameof(DocumentType.ShippingDocument)} , {nameof(DocumentType.OrderConfirmation)}, {nameof(DocumentType.Unknown)}" +
@ -342,10 +355,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
ShippingDocumentToFiles shippingDocumentToFiles = new ShippingDocumentToFiles ShippingDocumentToFiles shippingDocumentToFiles = new ShippingDocumentToFiles
{ {
ShippingDocumentId = shippingDocumentId, ShippingDocumentId = shippingDocumentId,
FilesId = dbFile.Id FilesId = dbFile.Id,
DocumentType = extractedMetaData.DocumentType != null ? (DocumentType)Enum.Parse(typeof(DocumentType), extractedMetaData.DocumentType) : DocumentType.Unknown
}; };
await _dbContext.ShippingDocumentToFiles.InsertAsync(shippingDocumentToFiles); await _dbContext.ShippingDocumentToFiles.InsertAsync(shippingDocumentToFiles);
// - IF WE DON'T HAVE PARTNERID ALREADY: read partner information // - IF WE DON'T HAVE PARTNERID ALREADY: read partner information
// (check if all 3 refers to the same partner) // (check if all 3 refers to the same partner)

View File

@ -30,6 +30,10 @@
@T("Admin.Orders") @T("Admin.Orders")
</h1> </h1>
<div class="float-right"> <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"> <div class="btn-group">
<button type="button" class="btn btn-success"> <button type="button" class="btn btn-success">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
@ -94,6 +98,7 @@
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.OrderListButtons, additionalData = Model }) @await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.OrderListButtons, additionalData = Model })
</div> </div>
</div> </div>
<section class="content"> <section class="content">
<div class="container-fluid"> <div class="container-fluid">
<div class="form-horizontal"> <div class="form-horizontal">
@ -677,3 +682,68 @@
</div> </div>
</div> </div>
@*create new order form*@
<div id="create-order-window" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="create-order-window-title">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="create-order-window-title">@T("Admin.Orders.AddNew")</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form asp-controller="Order" 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">
<div class="label-wrapper">
<label class="col-form-label">
@T("Admin.Orders.Fields.Customer")
</label>
</div>
</div>
<div class="col-md-9">
<input type="text" id="create-order-customer-search" autocomplete="off" class="form-control" placeholder="Type customer name or email..." />
<span id="create-order-customer-name" class="mt-2 d-inline-block"></span>
<input type="hidden" id="create-order-customer-id" name="customerId" value="" />
<span class="field-validation-error" id="create-order-customer-error" style="display:none;">Please select a customer</span>
</div>
</div>
</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" id="create-order-submit">
@T("Admin.Common.Create")
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<style>
/* Fix z-index for autocomplete dropdown in modal */
.ui-autocomplete {
z-index: 1060 !important; /* Bootstrap modal z-index is 1050 */
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
}
</style>
<script>
$('#create-order-customer-search').autocomplete({
delay: 500,
minLength: 2,
source: '@Url.Action("CustomerSearchAutoComplete", "CustomOrder")',
select: function(event, 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-order-customer-error').hide();
return false;
}
});
</script>

View File

@ -24,14 +24,22 @@
}) })
.Columns(c => { .Columns(c => {
c.Add().DataField("Id").AllowEditing(false); c.Add().DataField("Id").AllowEditing(false);
c.Add().DataField("Partner.Name").AllowEditing(false); c.Add().DataField("PartnerId")
.AllowEditing(true)
.Lookup(lookup => lookup
.DataSource(d => d.Mvc().Controller("ManagementPage").LoadAction("GetAllPartners").Key("Id"))
.ValueExpr("Id")
.DisplayExpr("Name")
)
.EditCellTemplate(new TemplateName("DropDownBoxTemplate"))
.Width(150);
c.Add() c.Add()
.Caption("Items in order") .Caption("Items in order")
.DataType(GridColumnDataType.Number) .DataType(GridColumnDataType.Number)
.CalculateCellValue("calculateItemsCount").AllowEditing(false); .CalculateCellValue("calculateItemsCount").AllowEditing(false);
c.Add().DataField("PartnerId"); @* c.Add().DataField("PartnerId"); *@
c.Add().DataField("DocumentIdNumber"); c.Add().DataField("DocumentIdNumber");
c.Add().DataField("IsAllMeasured"); c.Add().DataField("IsAllMeasured").AllowEditing(false);
c.Add() c.Add()
.Caption("Completed") .Caption("Completed")
.DataType(GridColumnDataType.Boolean) .DataType(GridColumnDataType.Boolean)
@ -54,8 +62,10 @@
); );
}); });
}) })
.MasterDetail(md => md.Enabled(true).Template(new TemplateName("masterDetailTemplate"))) .MasterDetail(md => md.Enabled(true).Template(new TemplateName("masterDetailTemplate"))
) )
)
</div> </div>
@using (Html.DevExtreme().NamedTemplate("masterDetailTemplate")) @using (Html.DevExtreme().NamedTemplate("masterDetailTemplate"))
@ -108,6 +118,40 @@
} }
} }
@using(Html.DevExtreme().NamedTemplate("DropDownBoxTemplate")) {
@(Html.DevExtreme().DropDownBox()
.DataSource(d => d.Mvc().Controller("ManagementPage").LoadAction("GetAllPartners").Key("Id"))
.Value(new JS("value"))
.ValueExpr("Id")
.InputAttr("aria-label", "Partner")
.DisplayExpr("Name")
.DropDownOptions(options => options.Width(500))
.Option("setValue", new JS("setValue"))
.ContentTemplate(new TemplateName("ContentTemplate"))
)
}
@using(Html.DevExtreme().NamedTemplate("ContentTemplate")) {
@(Html.DevExtreme().DataGrid()
.DataSource(d => d.Mvc().Controller("ManagementPage").LoadAction("GetAllPartners").Key("Id"))
.RemoteOperations(true)
.Height(250)
.Columns(c => {
c.Add().DataField("Name");
c.Add().DataField("Country");
c.Add().DataField("TaxId");
})
.Scrolling(s => s.Mode(GridScrollingMode.Virtual))
.HoverStateEnabled(true)
.Selection(s => s.Mode(SelectionMode.Single))
.SelectedRowKeys(new JS("component.option('value') !== undefined && component.option('value') !== null ? [component.option('value')] : []"))
.FocusedRowEnabled(true)
.FocusedRowKey(new JS("component.option('value')"))
.OnContextMenuPreparing("function(e) { e.items = [] }")
.OnSelectionChanged("function(e) { onPartnerSelectionChanged(e, component) }")
)
}
<script> <script>
// Store the parent grid model as JSON // Store the parent grid model as JSON
var parentGridModel = @Html.Raw(Json.Serialize(Model)); var parentGridModel = @Html.Raw(Json.Serialize(Model));
@ -180,10 +224,6 @@
}); });
} }
function onSelectionChanged(data) {
let dataGrid = $("#orderDataGridContainer").dxDataGrid("instance");
dataGrid.option("toolbar.items[1].options.disabled", !data.selectedRowsData.length);
}
function onRowExpanded(e) { function onRowExpanded(e) {
// Trigger loading of first tab when row expands // Trigger loading of first tab when row expands
@ -196,4 +236,20 @@
} }
} }
} }
function onInitNewRow(e) {
// Replace this with actual default values and db insert
}
function onPartnerSelectionChanged(e, dropDownBoxComponent) {
var selectedRowKey = e.selectedRowKeys[0];
if (e.selectedRowKeys.length > 0) {
dropDownBoxComponent.option('value', selectedRowKey);
dropDownBoxComponent.close();
}
}
</script> </script>

View File

@ -131,6 +131,11 @@ public class RouteProvider : IRouteProvider
name: "Plugin.FruitBank.Admin.Orders.SaveOrderAttributes", name: "Plugin.FruitBank.Admin.Orders.SaveOrderAttributes",
pattern: "Admin/CustomOrder/SaveOrderAttributes", pattern: "Admin/CustomOrder/SaveOrderAttributes",
defaults: new { controller = "CustomOrder", action = "SaveOrderAttributes", area = AreaNames.ADMIN }); defaults: new { controller = "CustomOrder", action = "SaveOrderAttributes", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Orders.CustomerSearchAutoComplete",
pattern: "Admin/CustomOrder/CustomerSearchAutoComplete",
defaults: new { controller = "CustomOrder", action = "CustomerSearchAutoComplete", area = AreaNames.ADMIN });
} }
/// <summary> /// <summary>

View File

@ -1,8 +1,9 @@
using System.Linq; using FruitBank.Common.Interfaces;
using FruitBank.Common.Interfaces; using Microsoft.AspNetCore.Http;
using Nop.Core; using Nop.Core;
using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Orders; using Nop.Core.Domain.Orders;
using Nop.Core.Events;
using Nop.Plugin.Misc.FruitBankPlugin.Models; using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Services.Catalog; using Nop.Services.Catalog;
using Nop.Services.Common; using Nop.Services.Common;
@ -13,10 +14,11 @@ 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 System.Linq;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{ {
public class EventConsumer : BaseAdminMenuCreatedEventConsumer, IConsumer<OrderPlacedEvent>, IConsumer<AdminMenuCreatedEvent> public class EventConsumer : BaseAdminMenuCreatedEventConsumer, IConsumer<OrderPlacedEvent>, IConsumer<EntityUpdatedEvent<Order>>, IConsumer<AdminMenuCreatedEvent>
{ {
private readonly IGenericAttributeService _genericAttributeService; private readonly IGenericAttributeService _genericAttributeService;
private readonly IProductService _productService; private readonly IProductService _productService;
@ -27,6 +29,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
private readonly IStoreContext _storeContext; private readonly IStoreContext _storeContext;
private readonly IAdminMenu _adminMenu; private readonly IAdminMenu _adminMenu;
private readonly ILocalizationService _localizationService; private readonly ILocalizationService _localizationService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly FruitBankAttributeService _fruitBankAttributeService;
public EventConsumer( public EventConsumer(
IGenericAttributeService genericAttributeService, IGenericAttributeService genericAttributeService,
@ -38,7 +42,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
IWorkContext workContext, IWorkContext workContext,
IStoreContext storeContext, IStoreContext storeContext,
IAdminMenu adminMenu, IAdminMenu adminMenu,
ILocalizationService localizationService) : base(pluginManager) ILocalizationService localizationService,
IHttpContextAccessor httpContextAccessor,
FruitBankAttributeService fruitBankAttributeService) : base(pluginManager)
{ {
_genericAttributeService = genericAttributeService; _genericAttributeService = genericAttributeService;
_productService = productService; _productService = productService;
@ -49,6 +55,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
_storeContext = storeContext; _storeContext = storeContext;
_adminMenu = adminMenu; _adminMenu = adminMenu;
_localizationService = localizationService; _localizationService = localizationService;
_httpContextAccessor = httpContextAccessor;
_fruitBankAttributeService = fruitBankAttributeService;
} }
protected override string PluginSystemName => "Misc.FruitBankPlugin"; protected override string PluginSystemName => "Misc.FruitBankPlugin";
@ -120,6 +128,39 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
} }
} }
public async Task HandleEventAsync(EntityUpdatedEvent<Order> eventMessage)
{
await SaveOrderCustomAttributesAsync(eventMessage.Entity);
}
private async Task SaveOrderCustomAttributesAsync(Order order)
{
if (order == null) return;
var form = _httpContextAccessor.HttpContext?.Request?.Form;
if (form == null || form.Count == 0) return;
if (form.ContainsKey(nameof(IMeasurable.IsMeasurable)))
{
var isMeasurable = form[nameof(IMeasurable.IsMeasurable)].ToString().Contains("true");
//var isMeasurable = CommonHelper.To<bool>(form[nameof(IMeasurable.IsMeasurable)].ToString());
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Order, bool>(order.Id, nameof(IMeasurable.IsMeasurable), isMeasurable);
}
if (form.ContainsKey(nameof(IOrderDto.DateOfReceipt)))
{
var dateOfReceipt = form[nameof(IOrderDto.DateOfReceipt)];
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Order, DateTime>(order.Id, nameof(IOrderDto.DateOfReceipt), DateTime.Parse(dateOfReceipt));
}
}
public async Task HandleEventAsync(AdminMenuCreatedEvent eventMessage) public async Task HandleEventAsync(AdminMenuCreatedEvent eventMessage)
{ {
var rootNode = eventMessage.RootMenuItem; var rootNode = eventMessage.RootMenuItem;

View File

@ -45,7 +45,7 @@
data: { data: {
orderId: "@Model.OrderId", orderId: "@Model.OrderId",
isMeasurable: $("#@Html.IdFor(m => m.IsMeasurable)").is(":checked"), isMeasurable: $("#@Html.IdFor(m => m.IsMeasurable)").is(":checked"),
pickupDateTimeUtc: $("#@Html.IdFor(m => m.DateOfReceipt)").val(), dateOfReceipt: $("#@Html.IdFor(m => m.DateOfReceipt)").val(),
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val() __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
}, },
success: function () { success: function () {