Compare commits

...

5 Commits

28 changed files with 6258 additions and 414 deletions

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components
{
[ViewComponent(Name = "FileUploadGridComponent")]
public class FileUploadGridComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(TestGridModel model)
{
// Here you can fetch data for this grid if needed
// For demo, just pass the model
return View(model.ViewComponentLocation, model);
}
}
}

View File

@ -0,0 +1,22 @@
// ViewComponent Class
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
using Nop.Services.Plugins;
using Org.BouncyCastle.Asn1.Ocsp;
using System.ComponentModel;
using System.Text.Json;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components
{
[ViewComponent(Name = "TestGridComponent")]
public class TestGridComponent : ViewComponent
{
public IViewComponentResult Invoke(TestGridModel model)
{
return View(model.ViewComponentLocation, model);
}
}
}

View File

@ -1,25 +0,0 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.ShippingDocumentListModel
@using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
@using FruitBank.Common.Entities;
@using DevExtreme.AspNet.Mvc
<h4>Id: @Model.ShippingId</h4>
@(Html.DevExtreme().DataGrid<ShippingDocument>()
.ID("documentsGrid")
.DataSource(Model.ShippingDocumentList)
.KeyExpr("Id")
.ShowBorders(true)
.Editing(editing => {
editing.Mode(GridEditMode.Row);
editing.AllowUpdating(true);
editing.AllowAdding(false);
editing.AllowDeleting(true);
})
.Columns(c => {
c.AddFor(m => m.DocumentDate).Caption("Date").DataType(GridDataType.Date);
c.AddFor(m => m.SenderName).Caption("Sender");
c.AddFor(m => m.InvoiceNumber).Caption("Invoice #");
c.AddFor(m => m.TotalAmount).Caption("Amount").DataType(GridDataType.Number);
c.AddFor(m => m.ItemCount).Caption("ItemCount").DataType(GridDataType.Number);
})
)

View File

@ -8,6 +8,8 @@ using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Plugin.Misc.FruitBankPlugin.Factories;
using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Services.Common;
using Nop.Services.Messages;
using Nop.Services.Orders;
using Nop.Services.Security;
using Nop.Web.Areas.Admin.Controllers;
@ -26,14 +28,18 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
private readonly CustomOrderModelFactory _orderModelFactory;
private readonly ICustomOrderSignalREndpointServer _customOrderSignalREndpoint;
private readonly IPermissionService _permissionService;
private readonly IGenericAttributeService _genericAttributeService;
private readonly INotificationService _notificationService;
// ... other dependencies
public CustomOrderController(IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService)
public CustomOrderController(IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService, IGenericAttributeService genericAttributeService, INotificationService notificationService)
{
_orderService = orderService;
_orderModelFactory = orderModelFactory as CustomOrderModelFactory;
_customOrderSignalREndpoint = customOrderSignalREndpoint;
_permissionService = permissionService;
_genericAttributeService = genericAttributeService;
_notificationService = notificationService;
// ... initialize other deps
}
@ -64,6 +70,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
//prepare model
var orderListModel = await GetOrderListModelByFilter(searchModel);
//var orderListModel = new OrderListModel();
var valami = Json(orderListModel);
Console.WriteLine(valami);
@ -100,6 +107,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// return Json(model);
//}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveOrderAttributes(OrderAttributesModel model)
{
if (!ModelState.IsValid)
{
// reload order page with errors
return RedirectToAction("Edit", "Order", new { id = model.OrderId });
}
var order = await _orderService.GetOrderByIdAsync(model.OrderId);
if (order == null)
return RedirectToAction("List", "Order");
// store attributes in GenericAttribute table
await _genericAttributeService.SaveAttributeAsync(order, nameof(OrderModelExtended.IsMeasurable), model.IsMeasurable);
await _genericAttributeService.SaveAttributeAsync(order, nameof(IOrderDto.DateOfReceipt), model.DateOfReceipt);
_notificationService.SuccessNotification("Custom attributes saved successfully.");
return RedirectToAction("Edit", "Order", new { id = model.OrderId });
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace DevExtreme.NETCore.Demos.Controllers
{
public class FileManagerController : Controller
{
public IActionResult BindingToFileSystem()
{
return View();
}
}
}

View File

@ -0,0 +1,48 @@
using DevExtreme.AspNet.Mvc.FileManagement;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using System.Collections.Generic;
namespace DevExtreme.NETCore.Demos.Controllers
{
public class FileManagerScriptsApiController : Controller
{
IWebHostEnvironment _webHostEnvironment;
public FileManagerScriptsApiController(IWebHostEnvironment webHostEnvironment)
{
_webHostEnvironment = webHostEnvironment;
}
[HttpGet]
[HttpPost]
[Route("api/file-manager-file-system", Name = "FileManagementFileSystemApi")]
public object FileSystem(FileSystemCommand command, string arguments, int orderId)
{
string path = Request.Headers["TestHeader"];
var valami = new List<int>();
var config = new FileSystemConfiguration
{
Request = Request,
FileSystemProvider = new PhysicalFileSystemProvider(_webHostEnvironment.ContentRootPath + $"/wwwroot/uploads/orders/order{path}"),
//uncomment the code below to enable file/folder management
//AllowCopy = true,
//AllowCreate = true,
//AllowMove = true,
//AllowDelete = true,
//AllowRename = true,
//AllowUpload = true,
//AllowDownload = true,
AllowedFileExtensions = new[] { ".pdf", ".jpg", ".jpeg" }
};
var processor = new FileSystemCommandProcessor(config);
var result = processor.Execute(command, arguments);
return result.GetClientCommandResult();
}
}
}

View File

@ -24,12 +24,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
private readonly IPermissionService _permissionService;
protected readonly FruitBankDbContext _dbContext;
protected readonly AICalculationService _aiCalculationService;
protected readonly OpenAIApiService _openAIApiService;
public ManagementPageController(IPermissionService permissionService, FruitBankDbContext fruitBankDbContext, AICalculationService aiCalculationService)
public ManagementPageController(IPermissionService permissionService, FruitBankDbContext fruitBankDbContext, AICalculationService aiCalculationService, OpenAIApiService openAIApiService)
{
_permissionService = permissionService;
_dbContext = fruitBankDbContext;
_aiCalculationService = aiCalculationService;
_openAIApiService = openAIApiService;
}
public async Task<IActionResult> Test()
@ -42,6 +44,31 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
var testGridModel2 = new TestGridModel();
testGridModel2.GridName = "Orders";
testGridModel2.ViewComponentName = "ShippingDocumentGridComponent";
testGridModel2.ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml";
testGridModel2.Configuration = new GridConfiguration();
testGridModel2.Configuration.ShowChildGridsAsTabs = true;
testGridModel2.ChildGrids = new List<TestGridModel>();
var childGrid1 = new TestGridModel
{
GridName = "TestGrid",
ViewComponentName = "TestGridComponent",
ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml",
ParentGridId = testGridModel2.Id,
ChildGrids = new List<TestGridModel>()
};
testGridModel2.ChildGrids.Add(childGrid1);
var childGrid2 = new TestGridModel
{
GridName = "Files",
ViewComponentName = "FileUploadGridComponent",
ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml",
ParentGridId = testGridModel2.Id,
ChildGrids = new List<TestGridModel>()
};
testGridModel2.ChildGrids.Add(childGrid2);
testPageModel.Grids.Add(testGridModel2);
var testGridModel = new TestGridModel();
@ -58,6 +85,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/Test.cshtml", testPageModel);
}
[HttpPost]
public IActionResult LoadChildGrid([FromBody] LoadChildGridRequest request)
{
// request.contextId is the actual row ID (data.Id from DevExtreme)
// request.childModel is the full TestGridModel object
// Add the context ID to the model's DataContext
if (request.ChildModel.DataContext == null)
request.ChildModel.DataContext = new Dictionary<string, int>();
request.ChildModel.DataContext["contextId"] = request.ContextId;
// Invoke the view component with the full model
return ViewComponent(request.ChildModel.ViewComponentName, request.ChildModel);
}
// Request model for deserialization
public class LoadChildGridRequest
{
public int ContextId { get; set; } // The actual row ID from data.Id
public TestGridModel ChildModel { get; set; } // The full child grid model
}
[HttpGet]
public async Task<IActionResult> GetShippings()
{
@ -135,11 +186,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
return Json(model);
}
[HttpPost]
[RequestSizeLimit(10485760)] // 10MB
[RequestFormLimits(MultipartBodyLengthLimit = 10485760)]
public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId, int? partnerId)
{
var shippingDocument = await _dbContext.ShippingDocuments.GetByIdAsync(shippingDocumentId);
//checks
// - files exist
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
@ -159,7 +213,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
}
var filesList = new List<Files>();
var shippingDocumentToFileList = new List<ShippingDocumentToFiles>();
//iteratation 1: iterate documents to determine their type by AI
@ -168,16 +221,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
foreach (var file in files)
{
var fileName = file.FileName;
var fileSize = file.Length;
var dbFile = new Files();
string pdfText = "";
Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}");
Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}, content type: {file.ContentType}");
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
return Json(new { success = false, errorMessage = "Only PDF files are allowed" });
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) && !file.ContentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase))
return Json(new { success = false, errorMessage = "Only PDF or jpg files are allowed" });
// Validate file size (max 20MB)
if (file.Length > 20 * 1024 * 1024)
@ -185,9 +237,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// - get text extracted from pdf
// Validate file type (PDF only)
if (file.Length > 0 && file.ContentType == "application/pdf")
//if (file.Length > 0 && file.ContentType == "application/pdf")
if (file.Length > 0)
{
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)){
try
{
// Open the PDF from the IFormFile's stream directly in memory
@ -200,6 +253,21 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
// Extract text from each page
pdfText += ContentOrderTextExtractor.GetText(page);
}
//still nothing? let's send it to AI
if (string.IsNullOrWhiteSpace(pdfText))
{
try
{
// ✅ Use the service we implemented earlier
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this PDF.");
}
catch (Exception aiEx)
{
Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
}
}
// For demonstration, let's just log the extracted text
@ -214,9 +282,46 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
return StatusCode(500, $"Error processing PDF file: {ex.Message}");
}
}
else
{
try
{
// Open the PDF from the IFormFile's stream directly in memory
using (var stream = file.OpenReadStream())
{
try
{
// ✅ Use the service we implemented earlier
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this PDF.");
}
catch (Exception aiEx)
{
Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
}
// For demonstration, let's just log the extracted text
Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}");
}
}
catch (Exception ex)
{
// Handle potential exceptions during PDF processing
Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}");
return StatusCode(500, $"Error processing PDF file: {ex.Message}");
}
}
//we should have some kind of text now
Console.WriteLine(pdfText);
}
string analysisPrompt = "Extract the document identification number from this document, determine the type of the " +
"document 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)}" +
"If you can't find information of any of these, return null value for that field.";
@ -226,11 +331,22 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
var extractedMetaData = ParseMetaDataAIResponse(metaAnalyzis);
if (extractedMetaData.DocumentNumber != null)
{
dbFile.RawText = pdfText;
dbFile.FileExtension = "pdf";
dbFile.FileName = extractedMetaData.DocumentNumber;
}
await _dbContext.Files.InsertAsync(dbFile);
filesList.Add(dbFile);
ShippingDocumentToFiles shippingDocumentToFiles = new ShippingDocumentToFiles
{
ShippingDocumentId = shippingDocumentId,
FilesId = dbFile.Id
};
await _dbContext.ShippingDocumentToFiles.InsertAsync(shippingDocumentToFiles);
// - IF WE DON'T HAVE PARTNERID ALREADY: read partner information
// (check if all 3 refers to the same partner)
// save partner information to partners table { Id, Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street }
@ -254,7 +370,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
Console.WriteLine(extractedPartnerData.TaxId);
}
if (extractedPartnerData.Country != null) {
if (extractedPartnerData.Country != null)
{
Console.WriteLine(extractedPartnerData.Country);
}

View File

@ -0,0 +1,10 @@
using Nop.Web.Framework.Models;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.Catalog;
/// <summary>
/// Represents a product list model
/// </summary>
public partial record ProductListModel : BasePagedListModel<ProductModel>
{
}

View File

@ -1,18 +1,185 @@
//using Nop.Web.Framework.Models;
//using Nop.Web.Framework.Mvc.ModelBinding;
//using System.ComponentModel.DataAnnotations;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
{
/// <summary>
/// Represents a grid with potential child grids
/// </summary>
public class TestGridModel
{
public Guid Id = Guid.NewGuid();
public TestGridModel()
{
Id = Guid.NewGuid();
ChildGrids = new List<TestGridModel>();
Configuration = new GridConfiguration();
}
// Identity
public Guid Id { get; set; }
public string GridName { get; set; }
//public string GridControllerName { get; set; }
//public string GridEndpointName { get; set; }
// View Component Information
public string ViewComponentName { get; set; }
public string ViewComponentLocation { get; set; }
public int? ParentRowId { get; set; }
// Hierarchy
public Guid? ParentGridId { get; set; }
public int Level { get; set; } // 0 = top level, 1 = first nested, etc.
public List<TestGridModel> ChildGrids { get; set; }
// Grid Behavior Configuration
public GridConfiguration Configuration { get; set; }
// Data Context (optional - for passing entity IDs or filter params)
public Dictionary<string, int> DataContext { get; set; }
}
/// <summary>
/// Configuration for grid rendering and behavior
/// </summary>
public class GridConfiguration
{
// Display Settings
public bool ShowChildGridsAsTabs { get; set; } = true;
public bool ShowChildGridsAsAccordion { get; set; } = false;
public bool ShowChildGridsInline { get; set; } = false;
// Rendering Options
public string ChildGridContainerCssClass { get; set; } = "nested-grid-container";
public bool LazyLoadChildren { get; set; } = false;
public bool CollapseByDefault { get; set; } = false;
// Data Loading
public string ChildDataEndpoint { get; set; }
public bool RequiresParentRowSelection { get; set; } = false;
// Metadata
public string Description { get; set; }
public int DisplayOrder { get; set; }
}
/// <summary>
/// Builder class for easier model construction
/// </summary>
public class TestPageModelBuilder
{
private readonly TestPageModel _model;
private readonly Dictionary<Guid, TestGridModel> _gridLookup;
public TestPageModelBuilder()
{
_model = new TestPageModel();
_gridLookup = new Dictionary<Guid, TestGridModel>();
}
public TestPageModelBuilder AddRootGrid(TestGridModel grid)
{
grid.Level = 0;
grid.ParentGridId = null;
_model.Grids.Add(grid);
_gridLookup[grid.Id] = grid;
return this;
}
public TestPageModelBuilder AddChildGrid(Guid parentId, TestGridModel childGrid)
{
if (_gridLookup.TryGetValue(parentId, out var parentGrid))
{
childGrid.Level = parentGrid.Level + 1;
childGrid.ParentGridId = parentId;
parentGrid.ChildGrids.Add(childGrid);
_gridLookup[childGrid.Id] = childGrid;
}
return this;
}
public TestPageModel Build()
{
return _model;
}
}
/// <summary>
/// Example usage helper
/// </summary>
public static class TestPageModelExample
{
public static TestPageModel CreateSampleModel()
{
var builder = new TestPageModelBuilder();
// Level 0 - Root Grids
var customersGrid = new TestGridModel
{
GridName = "Customers",
ViewComponentName = "CustomerGrid",
Configuration = new GridConfiguration
{
RequiresParentRowSelection = false,
Description = "Main customer list"
}
};
var ordersRootGrid = new TestGridModel
{
GridName = "All Orders",
ViewComponentName = "OrderGrid"
};
builder.AddRootGrid(customersGrid);
builder.AddRootGrid(ordersRootGrid);
// Level 1 - Child of Customers
var customerOrdersGrid = new TestGridModel
{
GridName = "Customer Orders",
ViewComponentName = "CustomerOrderGrid",
Configuration = new GridConfiguration
{
RequiresParentRowSelection = true,
ShowChildGridsAsTabs = true
}
};
var customerAddressesGrid = new TestGridModel
{
GridName = "Addresses",
ViewComponentName = "CustomerAddressGrid"
};
builder.AddChildGrid(customersGrid.Id, customerOrdersGrid);
builder.AddChildGrid(customersGrid.Id, customerAddressesGrid);
// Level 2 - Child of Customer Orders
var orderItemsGrid = new TestGridModel
{
GridName = "Order Items",
ViewComponentName = "OrderItemGrid",
Configuration = new GridConfiguration
{
RequiresParentRowSelection = true
}
};
var orderShipmentsGrid = new TestGridModel
{
GridName = "Shipments",
ViewComponentName = "OrderShipmentGrid"
};
builder.AddChildGrid(customerOrdersGrid.Id, orderItemsGrid);
builder.AddChildGrid(customerOrdersGrid.Id, orderShipmentsGrid);
// Level 3 - Child of Order Items
var itemAttributesGrid = new TestGridModel
{
GridName = "Item Attributes",
ViewComponentName = "ItemAttributeGrid"
};
builder.AddChildGrid(orderItemsGrid.Id, itemAttributesGrid);
return builder.Build();
}
}
}

View File

@ -6,6 +6,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
{
public class TestPageModel
{
public TestPageModel()
{
Grids = new List<TestGridModel>();
}
public List<TestGridModel> Grids { get; set; }
}
}

View File

@ -0,0 +1,112 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
@using DevExtreme.AspNet.Mvc
@{
var contextId = Model.DataContext["contextId"];
var fileManagerId = $"fileManager_{contextId}";
var beforeAjaxSendFunctionName = $"beforeAjaxSend_{contextId}";
}
<div class="row">
<div class="col-6">
<form method="post" enctype="multipart/form-data" asp-controller="ManagementPage" asp-action="UploadFile">
@(Html.DevExtreme().FileUploader()
.ID("shippingDocumentUploader-" + contextId)
.Name("files")
.Multiple(true)
.Accept("application/pdf")
.UploadMode(FileUploadMode.UseForm)
)
<input type="hidden" name="ShippingDocumentId" value="@contextId" />
<input type="hidden" name="PartnerId" value="hello" />
@(Html.DevExtreme().Button()
.Text("Upload Files")
.Type(ButtonType.Success)
.UseSubmitBehavior(true)
)
</form>
<div class="content" id="selected-files">
<div>
<h4>Selected Files</h4>
</div>
</div>
</div>
<div class="col-6">
@(Html.DevExtreme().FileManager()
.ID(fileManagerId)
.FileSystemProvider(provider => provider.Remote()
.Url(Url.RouteUrl("FileManagementFileSystemApi"))
.BeforeAjaxSend(@<text>
function(arg) {
arg.headers.TestHeader = @Model.DataContext["contextId"];
}
</text>))
.Permissions(permissions => {
permissions.Download(true);
permissions.Upload(true);
})
.AllowedFileExtensions(new[] { ".pdf", ".jpg", ".jpeg" })
)
</div>
</div>
<script>
function fileUploader_valueChanged(e) {
var files = e.value;
if(files.length > 0) {
$("#selected-files .selected-item").remove();
$.each(files, function(i, file) {
var $selectedItem = $("<div />").addClass("selected-item");
$selectedItem.append(
$("<span />").html("Name: " + file.name + "<br/>"),
$("<span />").html("Size " + file.size + " bytes" + "<br/>"),
$("<span />").html("Type " + file.type + "<br/>"),
$("<span />").html("Last Modified Date: " + file.lastModifiedDate)
);
$selectedItem.appendTo($("#selected-files"));
});
$("#selected-files").show();
}
else
$("#selected-files").hide();
}
function getGridInstance() {
return $("#shippingDocumentUploader").dxFileUploader("instance");
}
function fileUploader_fileUploaded(e) {
const fileUploaderId = e.component.element().attr('id');
// 2. Extract the number from the ID
const match = fileUploaderId.match(/\d+$/);
if (match) {
const uniqueId = match[0];
const gridId = `shippingDocumentGridContainer-${uniqueId}`;
// 3. Get the DevExtreme grid instance and refresh it
const grid = $(`#${gridId}`).dxDataGrid('instance');
if (grid) {
grid.dxDataGrid("getDataSource").reload();
// Optional: Show a success notification
DevExpress.ui.notify("Documents updated successfully!", "success", 2000);
} else {
console.error(`DevExtreme grid with ID "${gridId}" not found.`);
}
} else {
console.error("Could not find a unique ID number from the file uploader.");
}
// shippingDocumentGridContainer
//$("#shippingDocumentGridContainer" + e.component.ID).dxDataGrid("getDataSource").reload();
}
</script>

View File

@ -1,6 +1,7 @@
@model OrderSearchModel
@inject IStoreService storeService
@using FruitBank.Common.Interfaces
@using Nop.Plugin.Misc.FruitBankPlugin.Models
@using Nop.Services.Stores
@using Nop.Web.Areas.Admin.Components
@ -290,9 +291,7 @@
</div>
<div class="card card-default">
<div class="card-header">
<h1>RTTTTTTTTTT</h1>
</div>
<div class="card-body">
<nop-doc-reference asp-string-resource="@T("Admin.Documentation.Reference.Orders", Docs.Orders + Utm.OnAdmin)" />
@ -342,9 +341,16 @@
};
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable))
{
Title = "Needs Measurement",
Title = T("Admin.Orders.Fields.ToBeMeasured").Text,
Width = "100",
Render = new RenderCustom("renderColumnNeedsMeasurement"),
Render = new RenderCustom("renderColumnIsMeasurable"),
ClassName = NopColumnClassDefaults.CenterAll
});
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(IOrderDto.DateOfReceipt))
{
Title = T("Admin.Orders.Fields.PickupDate").Text,
Width = "100",
Render = new RenderCustom("renderColumnPickupDateAndTime"),
ClassName = NopColumnClassDefaults.CenterAll
});
@ -441,11 +447,16 @@
return `${textRenderer(row.CustomerFullName)} <br /><a href="${link}">${data}</a > `;
}
function renderColumnNeedsMeasurement(data, type, row, meta) {
function renderColumnIsMeasurable(data, type, row, meta) {
if(data === true) {
return '<span class="badge badge-warning">Yes</span>';
return '<span class="badge badge-warning" disabled>Yes</span>';
}
return '<span class="badge badge-secondary">No</span>';
return '<span class="badge badge-secondary" disabled>No</span>';
}
function renderColumnPickupDateAndTime(data, type, row, meta) {
return `<span>${data}</span>`;
}
$(function() {

View File

@ -1,10 +1,6 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
@using DevExtreme.AspNet.Mvc
@{
var contextId = Model;
// var gridId = $"dataGrid_{Guid.NewGuid():N}";
}
@Html.AntiForgeryToken()
<div>
@(
@ -27,7 +23,6 @@
editing.AllowDeleting(true);
})
.Columns(c => {
c.Add().DataField("Id").AllowEditing(false);
c.Add().DataField("Partner.Name").AllowEditing(false);
c.Add()
@ -37,7 +32,6 @@
c.Add().DataField("PartnerId");
c.Add().DataField("DocumentIdNumber");
c.Add().DataField("IsAllMeasured");
c.Add()
.Caption("Completed")
.DataType(GridColumnDataType.Boolean)
@ -60,244 +54,146 @@
);
});
})
.MasterDetail(md => {
md.Enabled(true);
md.Template(@<text>
<div class="master-detail-caption"><%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:</div>
<div id="fileuploader">
<div class="widget-container">
<section id="tabs" class="project-tab">
<div class="row">
<div class="col-md-12">
<nav>
<div class="nav nav-tabs nav-fill" id="nav-tab" role="tablist">
<a class="nav-item nav-link active" id="nav-home-tab" data-toggle="tab" href="#nav-home" role="tab" aria-controls="nav-home" aria-selected="true">Partner info</a>
<a class="nav-item nav-link" id="nav-profile-tab" data-toggle="tab" href="#nav-profile" role="tab" aria-controls="nav-profile" aria-selected="false">Products</a>
<a class="nav-item nav-link" id="nav-contact-tab" data-toggle="tab" href="#nav-contact" role="tab" aria-controls="nav-contact" aria-selected="false">Files</a>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab">
<table class="table" cellspacing="0">
<thead>
<tr>
<th>Project Name</th>
<th>Employer</th>
<th>Awards</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="#">Work 1</a></td>
<td>Doe</td>
<td>john@example.com</td>
</tr>
<tr>
<td><a href="#">Work 2</a></td>
<td>Moe</td>
<td>mary@example.com</td>
</tr>
<tr>
<td><a href="#">Work 3</a></td>
<td>Dooley</td>
<td>july@example.com</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab">
<table class="table" cellspacing="0">
<thead>
<tr>
<th>Project Name</th>
<th>Employer</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="#">Work 1</a></td>
<td>Doe</td>
<td>john@example.com</td>
</tr>
<tr>
<td><a href="#">Work 2</a></td>
<td>Moe</td>
<td>mary@example.com</td>
</tr>
<tr>
<td><a href="#">Work 3</a></td>
<td>Dooley</td>
<td>july@example.com</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="nav-contact" role="tabpanel" aria-labelledby="nav-contact-tab">
<form method="post" enctype="multipart/form-data" asp-controller="ManagementPage" asp-action="UploadFile">
@(Html.DevExtreme().FileUploader()
.ID(new JS("'shippingDocumentUploader-' + data.Id"))
.Name("files")
.Multiple(true)
.Accept("application/pdf")
.UploadMode(FileUploadMode.UseForm)
.MasterDetail(md => md.Enabled(true).Template(new TemplateName("masterDetailTemplate")))
)
<input type="hidden" name="ShippingDocumentId" value="<%- data.Id %>" />
<% if (data.PartnerId) { %>
<input type="hidden" name="PartnerId" value="<%- data.PartnerId %>" />
<% } %>
@(Html.DevExtreme().Button()
.Text("Upload Files")
.Type(ButtonType.Success)
.UseSubmitBehavior(true)
)
</form>
<div class="content" id="selected-files">
<div>
<h4>Selected Files</h4>
</div>
</div>
@* <div id="@(new JS("'shippingDocumentGridContainer-' + data.ID"))"></div>
@(Html.DevExtreme().DataGrid<FruitBank.Common.Entities.ShippingDocument>()
.ColumnAutoWidth(true)
.ShowBorders(true)
.ID(new JS("'shippingDocumentGridContainer-' + data.Id"))
.Columns(columns => {
columns.AddFor(m => m.Id).AllowEditing(false);
columns.AddFor(m => m.Country);
columns.AddFor(m => m.Created);
columns.AddFor(m => m.PartnerId);
columns.Add()
.Caption("Completed")
.DataType(GridColumnDataType.Boolean)
.CalculateCellValue("calculateCellValue");
})
.DataSource(ds => ds.Mvc()
.Controller("Shipping")
.LoadAction("GetShippingDocumentsByShippingId")
.LoadParams(new { shippingId = new JS("data.Id") })
.Key("Id")
)
) *@
</div>
</div>
</div>
@using (Html.DevExtreme().NamedTemplate("masterDetailTemplate"))
{
<div class="master-detail-caption">
<%- data.ShippingDate %> <%- data.LicencePlate %>'s shipping documents:
</div>
</section>
@if (Model.ChildGrids != null && Model.ChildGrids.Any())
{
<div class="@Model.Configuration.ChildGridContainerCssClass mt-3" data-parent-id="<%- data.Id %>">
@if (Model.Configuration.ShowChildGridsAsTabs)
{
<ul class="nav nav-tabs" role="tablist">
@for (int i = 0; i < Model.ChildGrids.Count; i++)
{
var child = Model.ChildGrids[i];
var isActive = i == 0 ? "active" : "";
<li class="nav-item">
<a class="nav-link @isActive"
data-toggle="tab"
data-child-index="@i"
href="#content-@child.Id-<%- data.Id %>"
onclick="reloadChildGrid(this, '<%- data.Id %>', @i)">
@child.GridName
</a>
</li>
}
</ul>
<div class="tab-content mt-2">
@for (int i = 0; i < Model.ChildGrids.Count; i++)
{
var child = Model.ChildGrids[i];
var isActive = i == 0 ? "show active" : "";
<div class="tab-pane fade @isActive"
id="content-@child.Id-<%- data.Id %>"
data-child-index="@i">
@* Initial load - will be replaced by AJAX on first tab click *@
<div class="text-center p-3">
<span class="spinner-border spinner-border-sm" role="status"></span>
Loading...
</div>
</div>
</text>);
})
)
</div>
}
</div>
}
</div>
}
}
<script>
// Store the parent grid model as JSON
var parentGridModel = @Html.Raw(Json.Serialize(Model));
// Global function to reload child grids
function reloadChildGrid(tabElement, contextId, childIndex) {
const $tab = $(tabElement);
const $contentPane = $($tab.attr('href'));
// Check if already loaded
if ($contentPane.data('loaded')) {
return;
}
// Show loading state
$contentPane.html('<div class="text-center p-3"><span class="spinner-border spinner-border-sm"></span> Loading...</div>');
// Get the child model from the parent model
var childModel = parentGridModel.ChildGrids[childIndex];
$.ajax({
url: '@Url.Action("LoadChildGrid", "ManagementPage")',
type: 'POST',
contentType: 'application/json',
headers: {
'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val()
},
data: JSON.stringify({
contextId: contextId,
childModel: childModel
}),
success: function (data) {
$contentPane.html(data);
$contentPane.data('loaded', true);
},
error: function (xhr, status, error) {
$contentPane.html('<div class="alert alert-danger">Error loading grid: ' + error + '</div>');
}
});
}
// Load first tab automatically when master detail opens
$(document).ready(function() {
$('#orderDataGridContainer').on('contentReady', function() {
// This will trigger when master detail rows are expanded
$('.nav-tabs .nav-link.active').each(function() {
const contextId = $(this).closest('[data-parent-id]').attr('data-parent-id');
const childIndex = $(this).data('child-index');
if (contextId && childIndex !== undefined) {
reloadChildGrid(this, contextId, childIndex);
}
});
});
});
function calculateCellValue(rowData) {
return rowData.Status === "Completed";
}
</script>
<script>
function calculateItemsCount(rowData) {
return rowData.ShippingItems.length;
return rowData.ShippingItems ? rowData.ShippingItems.length : 0;
}
</script>
<script>
function onDeleteBtnClick(){
let dataGrid = $("#gridContainer").dxDataGrid("instance");
function onDeleteBtnClick() {
let dataGrid = $("#orderDataGridContainer").dxDataGrid("instance");
$.when.apply($, dataGrid.getSelectedRowsData().map(function(data) {
return dataGrid.getDataSource().store().remove(data.ID);
return dataGrid.getDataSource().store().remove(data.Id);
})).done(function() {
dataGrid.refresh();
});
}
function calculateFilterExpression(filterValue, selectedFilterOperation, target) {
if(target === "search" && typeof(filterValue) === "string") {
return [this.dataField, "contains", filterValue]
}
return function(data) {
return (data.AssignedEmployee || []).indexOf(filterValue) !== -1
}
}
function onSelectionChanged(data) {
let dataGrid = $("#gridContainer").dxDataGrid("instance");
let dataGrid = $("#orderDataGridContainer").dxDataGrid("instance");
dataGrid.option("toolbar.items[1].options.disabled", !data.selectedRowsData.length);
}
function onRowExpanded(e) {
e.component.dxDataGrid("getDataSource").reload();
// Trigger loading of first tab when row expands
const $firstTab = $(e.element).find('.master-detail-caption').next().find('.nav-link.active').first();
if ($firstTab.length) {
const contextId = e.key; // This is the actual row's Id (data.Id)
const childIndex = $firstTab.data('child-index');
if (contextId && childIndex !== undefined) {
setTimeout(() => reloadChildGrid($firstTab[0], contextId, childIndex), 100);
}
}
}
</script>
<script>
function fileUploader_valueChanged(e) {
var files = e.value;
if(files.length > 0) {
$("#selected-files .selected-item").remove();
$.each(files, function(i, file) {
var $selectedItem = $("<div />").addClass("selected-item");
$selectedItem.append(
$("<span />").html("Name: " + file.name + "<br/>"),
$("<span />").html("Size " + file.size + " bytes" + "<br/>"),
$("<span />").html("Type " + file.type + "<br/>"),
$("<span />").html("Last Modified Date: " + file.lastModifiedDate)
);
$selectedItem.appendTo($("#selected-files"));
});
$("#selected-files").show();
}
else
$("#selected-files").hide();
}
function getGridInstance() {
return $("#shippingDocumentUploader").dxFileUploader("instance");
}
function fileUploader_fileUploaded(e) {
const fileUploaderId = e.component.element().attr('id');
// 2. Extract the number from the ID
const match = fileUploaderId.match(/\d+$/);
if (match) {
const uniqueId = match[0];
const gridId = `shippingDocumentGridContainer-${uniqueId}`;
// 3. Get the DevExtreme grid instance and refresh it
const grid = $(`#${gridId}`).dxDataGrid('instance');
if (grid) {
grid.dxDataGrid("getDataSource").reload();
// Optional: Show a success notification
DevExpress.ui.notify("Documents updated successfully!", "success", 2000);
} else {
console.error(`DevExtreme grid with ID "${gridId}" not found.`);
}
} else {
console.error("Could not find a unique ID number from the file uploader.");
}
// shippingDocumentGridContainer
//$("#shippingDocumentGridContainer" + e.component.ID).dxDataGrid("getDataSource").reload();
}
</script>

View File

@ -0,0 +1,117 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
@using System.Text.Json
<div class="card">
<div class="card-header bg-info text-white">
<h4>Test Child Grid View Component</h4>
</div>
<div class="card-body">
<h5>Grid Information:</h5>
<table class="table table-bordered">
<tr>
<td><strong>Grid ID:</strong></td>
<td>@Model.Id</td>
</tr>
<tr>
<td><strong>Grid Name:</strong></td>
<td>@Model.GridName</td>
</tr>
<tr>
<td><strong>View Component Name:</strong></td>
<td>@Model.ViewComponentName</td>
</tr>
<tr>
<td><strong>Level:</strong></td>
<td>@Model.Level</td>
</tr>
<tr>
<td><strong>Parent Grid ID:</strong></td>
<td>@(Model.ParentGridId?.ToString() ?? "None")</td>
</tr>
</table>
@if (Model.DataContext != null && Model.DataContext.Any())
{
<h5 class="mt-3">Data Context:</h5>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Type</th>
</tr>
</thead>
<tbody>
@foreach (var kvp in Model.DataContext)
{
<tr>
<td><strong>@kvp.Key</strong></td>
<td>@kvp.Value</td>
<td><em>@kvp.Value.GetType().Name</em></td>
</tr>
}
</tbody>
</table>
@if (Model.DataContext.ContainsKey("contextId"))
{
<div class="alert alert-success mt-3">
<strong>Context ID Found:</strong> @Model.DataContext["contextId"]
</div>
}
else
{
<div class="alert alert-warning mt-3">
<strong>Warning:</strong> No contextId found in DataContext
</div>
}
}
else
{
<div class="alert alert-warning mt-3">
<strong>No Data Context available</strong>
</div>
}
@if (Model.Configuration != null)
{
<h5 class="mt-3">Configuration:</h5>
<table class="table table-bordered">
<tr>
<td><strong>Show Child Grids As Tabs:</strong></td>
<td>@Model.Configuration.ShowChildGridsAsTabs</td>
</tr>
<tr>
<td><strong>Requires Parent Row Selection:</strong></td>
<td>@Model.Configuration.RequiresParentRowSelection</td>
</tr>
<tr>
<td><strong>Description:</strong></td>
<td>@(Model.Configuration.Description ?? "N/A")</td>
</tr>
</table>
}
@if (Model.ChildGrids != null && Model.ChildGrids.Any())
{
<div class="alert alert-info mt-3">
<strong>Child Grids:</strong> This grid has @Model.ChildGrids.Count child grid(s)
<ul class="mb-0 mt-2">
@foreach (var child in Model.ChildGrids)
{
<li>@child.GridName (Level @child.Level)</li>
}
</ul>
</div>
}
else
{
<div class="alert alert-secondary mt-3">
<strong>No child grids</strong>
</div>
}
<h5 class="mt-3">Full Model JSON:</h5>
<pre class="bg-light p-3" style="max-height: 300px; overflow-y: auto;">@JsonSerializer.Serialize(Model, new JsonSerializerOptions { WriteIndented = true })</pre>
</div>
</div>

View File

@ -0,0 +1,466 @@
@model ProductSearchModel
@using Nop.Core.Domain.Catalog;
@{
//page title
ViewBag.PageTitle = T("Admin.Catalog.Products").Text;
//active menu item (system name)
NopHtml.SetActiveMenuItemSystemName("Products");
}
@{
const string hideSearchBlockAttributeName = "ProductListPage.HideSearchBlock";
var hideSearchBlock = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), hideSearchBlockAttributeName);
}
@if (Model.LicenseCheckModel.BlockPages != true)
{
<form asp-controller="Product" asp-action="List" method="post">
<div class="content-header clearfix">
<h1 class="float-left">
Fruitbank @T("Admin.Catalog.Products")
</h1>
<div class="float-right">
<a asp-action="Create" class="btn btn-primary">
<i class="fas fa-square-plus"></i>
@T("Admin.Common.AddNew")
</a>
<a asp-action="BulkEdit" class="btn btn-info">
<i class="fas fa-pen"></i>
@T("Admin.Catalog.Products.BulkEdit")
</a>
<button asp-action="DownloadCatalogPDF" type="submit" name="download-catalog-pdf" class="btn bg-purple">
<i class="far fa-file-pdf"></i>
@T("Admin.Catalog.Products.List.DownloadPDF")
</button>
<div class="btn-group">
<button type="button" class="btn btn-success">
<i class="fas fa-download"></i>
@T("Admin.Common.Export")
</button>
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">&nbsp;</span>
</button>
<ul class="dropdown-menu" role="menu">
<li class="dropdown-item">
<button asp-action="ExportToXml" type="submit" name="exportxml-all">
<i class="far fa-file-code"></i>
@T("Admin.Common.ExportToXml.All")
</button>
</li>
<li class="dropdown-item">
<button type="button" id="exportxml-selected">
<i class="far fa-file-code"></i>
@T("Admin.Common.ExportToXml.Selected")
</button>
</li>
<li class="dropdown-divider"></li>
<li class="dropdown-item">
<button asp-action="ExportToExcel" type="submit" name="exportexcel-all">
<i class="far fa-file-excel"></i>
@T("Admin.Common.ExportToExcel.All")
</button>
</li>
<li class="dropdown-item">
<button type="button" id="exportexcel-selected">
<i class="far fa-file-excel"></i>
@T("Admin.Common.ExportToExcel.Selected")
</button>
</li>
</ul>
</div>
@if (!Model.IsLoggedInAsVendor || Model.AllowVendorsToImportProducts)
{
//a vendor cannot import products
<button type="button" name="importexcel" class="btn bg-olive" data-toggle="modal" data-target="#importexcel-window">
<i class="fas fa-upload"></i>
@T("Admin.Common.Import")
</button>
}
<button type="button" id="delete-selected" class="btn btn-danger">
<i class="far fa-trash-can"></i>
@T("Admin.Common.Delete.Selected")
</button>
<nop-action-confirmation asp-button-id="delete-selected" />
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.ProductListButtons, additionalData = Model })
</div>
</div>
<section class="content">
<div class="container-fluid">
<div class="form-horizontal">
<div class="cards-group">
<div class="card card-default card-search">
<div class="card-body">
<div class="row search-row @(!hideSearchBlock ? "opened" : "")" data-hideAttribute="@hideSearchBlockAttributeName">
<div class="search-text">@T("Admin.Common.Search")</div>
<div class="icon-search"><i class="fas fa-magnifying-glass" aria-hidden="true"></i></div>
<div class="icon-collapse"><i class="far fa-angle-@(!hideSearchBlock ? "up" : "down")" aria-hidden="true"></i></div>
</div>
<div class="search-body @(hideSearchBlock ? "closed" : "")">
<div class="row">
<div class="col-md-5">
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="SearchProductName" />
</div>
<div class="col-md-8">
<nop-editor asp-for="SearchProductName" />
</div>
</div>
<div class="form-group row" @(Model.AvailableCategories.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchCategoryId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchCategoryId" asp-items="Model.AvailableCategories" />
</div>
</div>
<div class="form-group row" @(Model.AvailableCategories.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchIncludeSubCategories" />
</div>
<div class="col-md-8">
<nop-editor asp-for="SearchIncludeSubCategories" />
</div>
</div>
<div class="form-group row" @(Model.AvailableManufacturers.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchManufacturerId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchManufacturerId" asp-items="Model.AvailableManufacturers" />
</div>
</div>
<div class="form-group row" @(Model.AvailableVendors.SelectionIsNotPossible() || Model.IsLoggedInAsVendor ? Html.Raw("style='display: none;'") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchVendorId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchVendorId" asp-items="Model.AvailableVendors" />
</div>
</div>
</div>
<div class="col-md-7">
<div class="form-group row" @(Model.HideStoresList ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchStoreId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchStoreId" asp-items="Model.AvailableStores" />
</div>
</div>
<div class="form-group row" @(Model.AvailableWarehouses.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchWarehouseId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchWarehouseId" asp-items="Model.AvailableWarehouses" />
</div>
</div>
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="SearchProductTypeId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchProductTypeId" asp-items="Model.AvailableProductTypes" />
</div>
</div>
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="SearchPublishedId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchPublishedId" asp-items="Model.AvailablePublishedOptions" />
</div>
</div>
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="GoDirectlyToSku" />
</div>
<div class="col-md-8">
<div class="input-group input-group-short">
<nop-editor asp-for="GoDirectlyToSku" />
<span class="input-group-append">
<button type="submit" id="go-to-product-by-sku" name="go-to-product-by-sku" class="btn btn-info btn-flat">
@T("Admin.Common.Go")
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="text-center col-12">
<button type="button" id="search-products" class="btn btn-primary btn-search">
<i class="fas fa-magnifying-glass"></i>
@T("Admin.Common.Search")
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card card-default">
<div class="card-body">
<nop-doc-reference asp-string-resource="@T("Admin.Documentation.Reference.Products", Docs.Products + Utm.OnAdmin)" />
@await Html.PartialAsync("Table", new DataTablesModel
{
Name = "products-grid",
UrlRead = new DataUrl("ProductList", "CustomProduct", null),
SearchButtonId = "search-products",
Length = Model.PageSize,
LengthMenu = Model.AvailablePageSizes,
Filters = new List<FilterParameter>
{
new FilterParameter(nameof(Model.SearchProductName)),
new FilterParameter(nameof(Model.SearchCategoryId)),
new FilterParameter(nameof(Model.SearchIncludeSubCategories), typeof(bool)),
new FilterParameter(nameof(Model.SearchManufacturerId)),
new FilterParameter(nameof(Model.SearchStoreId)),
new FilterParameter(nameof(Model.SearchWarehouseId)),
new FilterParameter(nameof(Model.SearchVendorId)),
new FilterParameter(nameof(Model.SearchProductTypeId)),
new FilterParameter(nameof(Model.SearchPublishedId))
},
ColumnCollection = new List<ColumnProperty>
{
new ColumnProperty(nameof(ProductModel.Id))
{
IsMasterCheckBox = true,
Render = new RenderCheckBox("checkbox_products"),
ClassName = NopColumnClassDefaults.CenterAll,
Width = "50"
},
new ColumnProperty(nameof(ProductModel.PictureThumbnailUrl))
{
Title = T("Admin.Catalog.Products.Fields.PictureThumbnailUrl").Text,
Width = "100",
Render = new RenderPicture(width: 100)
},
new ColumnProperty(nameof(ProductModel.Name))
{
Title = T("Admin.Catalog.Products.Fields.Name").Text
},
new ColumnProperty(nameof(ProductModel.Sku))
{
Title = T("Admin.Catalog.Products.Fields.Sku").Text,
Width = "100"
},
new ColumnProperty(nameof(ProductModel.FormattedPrice))
{
Title = T("Admin.Catalog.Products.Fields.Price").Text
},
new ColumnProperty(nameof(ProductModel.StockQuantityStr))
{
Title = T("Admin.Catalog.Products.Fields.StockQuantity").Text
},
new ColumnProperty(nameof(ProductModel.Published))
{
Title = T("Admin.Catalog.Products.Fields.Published").Text,
Width = "80",
ClassName = NopColumnClassDefaults.CenterAll,
Render = new RenderBoolean()
},
new ColumnProperty(nameof(ProductModel.Id))
{
Title = T("Admin.Common.Edit").Text,
Width = "80",
ClassName = NopColumnClassDefaults.Button,
Render = new RenderButtonEdit(new DataUrl("~/Admin/Product/Edit"))
}
}
})
<script>
$(function() {
$('#delete-selected-action-confirmation-submit-button').bind('click', function () {
var postData = {
selectedIds: selectedIds
};
addAntiForgeryToken(postData);
$.ajax({
cache: false,
type: "POST",
url: "@(Url.Action("DeleteSelected", "Product"))",
data: postData,
error: function (jqXHR, textStatus, errorThrown) {
showAlert('deleteSelectedFailed', errorThrown);
},
complete: function (jqXHR, textStatus) {
if (jqXHR.status === 204)
{
showAlert('nothingSelectedAlert', '@T("Admin.Common.Alert.NothingSelected")');
return;
}
updateTable('#products-grid');
}
});
$('#delete-selected-action-confirmation').modal('toggle');
return false;
});
});
</script>
<nop-alert asp-alert-id="deleteSelectedFailed" />
<nop-alert asp-alert-id="nothingSelectedAlert" />
</div>
</div>
</div>
</div>
</div>
</section>
<script>
$(function() {
$("#@Html.IdFor(model => model.GoDirectlyToSku)").keydown(function (event) {
if (event.keyCode === 13) {
$("#go-to-product-by-sku").trigger("click");
return false;
}
});
});
</script>
</form>
}
<script>
$(function() {
var displayModal = @((Model.LicenseCheckModel?.DisplayWarning == true || Model.LicenseCheckModel?.BlockPages == true).ToString().ToLower());
if (displayModal) {
$('#license-window').modal({
backdrop: 'static',
keyboard: false
});
$('#license-window').on('shown.bs.modal', function (event) {
var modalCloseEl = $(this).find('button.close');
var closeTextEl = $('span', modalCloseEl);
var startFrom = 5;
closeTextEl.text(startFrom);
const timer = setInterval(function() {
if (startFrom-- > 0)
closeTextEl.text(startFrom);
}, 1000);
setTimeout(function() {
closeTextEl.html('&times;');
modalCloseEl.on('click', function() {
$('#license-window').modal('hide')
});
clearInterval(timer);
}, startFrom*1000);
});
}
});
</script>
<div id="license-window" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
@Html.Raw(Model.LicenseCheckModel?.WarningText)
</div>
</div>
</div>
@*import products form*@
<div id="importexcel-window" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="importexcel-window-title">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="importexcel-window-title">@T("Admin.Common.ImportFromExcel")</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form asp-controller="Product" asp-action="ImportExcel" method="post" enctype="multipart/form-data">
<div class="form-horizontal">
<div class="modal-body">
<ul class="common-list">
<li>
<em>@T("Admin.Catalog.Products.List.ImportFromExcelTip")</em>
</li>
<li>
<em>@T("Admin.Common.ImportFromExcel.ManyRecordsWarning")</em>
</li>
</ul>
<div class="form-group row">
<div class="col-md-2">
<div class="label-wrapper">
<label class="col-form-label">
@T("Admin.Common.ExcelFile")
</label>
</div>
</div>
<div class="col-md-10">
<input type="file" id="importexcelfile" name="importexcelfile" class="form-control" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">
@T("Admin.Common.ImportFromExcel")
</button>
</div>
</div>
</form>
</div>
</div>
</div>
@*export selected (XML). We don't use GET approach because it's limited to 2K-4K chars and won't work for large number of entities*@
<form asp-controller="Product" asp-action="ExportXmlSelected" method="post" id="export-xml-selected-form">
<input type="hidden" id="selectedIds" name="selectedIds" value="" />
</form>
<script>
$(function() {
$('#exportxml-selected').click(function (e) {
e.preventDefault();
var ids = selectedIds.join(",");
if (!ids) {
$('#exportXmlSelected-info').text("@T("Admin.Products.NoProducts")");
$("#exportXmlSelected").trigger("click");
}
else {
$('#export-xml-selected-form #selectedIds').val(ids);
$('#export-xml-selected-form').submit();
updateTable('#products-grid');
}
return false;
});
});
</script>
<nop-alert asp-alert-id="exportXmlSelected" />
@*export selected (Excel). We don't use GET approach because it's limited to 2K-4K chars and won't work for large number of entities*@
<form asp-controller="Product" asp-action="ExportExcelSelected" method="post" id="export-excel-selected-form">
<input type="hidden" id="selectedIds" name="selectedIds" value="" />
</form>
<script>
$(function() {
$('#exportexcel-selected').click(function (e) {
e.preventDefault();
var ids = selectedIds.join(",");
if (!ids) {
$('#exportExcelSelected-info').text("@T("Admin.Products.NoProducts")");
$("#exportExcelSelected").trigger("click");
}
else {
$('#export-excel-selected-form #selectedIds').val(ids);
$('#export-excel-selected-form').submit();
updateTable('#products-grid');
}
return false;
});
});
</script>
<nop-alert asp-alert-id="exportExcelSelected" />

View File

@ -0,0 +1,55 @@
// File: Plugins/YourCompany.ProductAttributes/Components/ProductAttributesViewComponent.cs
using FruitBank.Common.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Plugin.Misc.FruitBankPlugin.Services;
using Nop.Services.Common;
using Nop.Web.Areas.Admin.Models.Catalog;
using Nop.Web.Areas.Admin.Models.Orders;
using Nop.Web.Framework.Components;
namespace Nop.Plugin.Misc.FruitBankPlugin.Components
{
[ViewComponent(Name = "OrderAttributes")]
public class OrderAttributesViewComponent : NopViewComponent
{
private readonly FruitBankAttributeService _fruitBankAttributeService;
private readonly IWorkContext _workContext;
private readonly IStoreContext _storeContext;
public OrderAttributesViewComponent(FruitBankAttributeService fruitBankAttributeService, IWorkContext workContext, IStoreContext storeContext)
{
_workContext = workContext;
_storeContext = storeContext;
_fruitBankAttributeService = fruitBankAttributeService;
}
public async Task<IViewComponentResult> InvokeAsync(string widgetZone, object additionalData)
{
if (additionalData is not OrderModel orderModel) return Content("");
var model = new OrderAttributesModel { OrderId = orderModel.Id };
if (model.OrderId > 0)
{
var orderPickupAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Order, DateTime?>(model.OrderId, nameof(IOrderDto.DateOfReceipt));
var orderMeasurableAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Order, bool>(model.OrderId, nameof(OrderModelExtended.IsMeasurable));
model.IsMeasurable = orderMeasurableAttributeValue;
if(orderPickupAttributeValue.HasValue && orderPickupAttributeValue.Value != DateTime.MinValue)
{
model.DateOfReceipt = orderPickupAttributeValue;
}
else
{
model.DateOfReceipt = null;
}
}
return View("~/Plugins/Misc.FruitBankPlugin/Views/OrderAttributes.cshtml", model);
}
}
}

View File

@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
using FruitBank.Common.Interfaces;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.TagHelpers;
@ -41,6 +42,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
public class CustomOrderModelFactory : OrderModelFactory
{
private readonly IOrderMeasurementService _orderMeasurementService;
private readonly IGenericAttributeService _genericAttributeService;
public CustomOrderModelFactory(
IOrderMeasurementService orderMeasurementService,
@ -88,7 +90,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
OrderSettings orderSettings,
ShippingSettings shippingSettings,
IUrlRecordService urlRecordService,
TaxSettings taxSettings
TaxSettings taxSettings,
IGenericAttributeService genericAttributeService
) : base(addressSettings,
catalogSettings,
currencySettings,
@ -137,6 +140,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
)
{
_orderMeasurementService = orderMeasurementService;
_genericAttributeService = genericAttributeService;
}
public override async Task<OrderSearchModel> PrepareOrderSearchModelAsync(OrderSearchModel searchModel)
@ -169,7 +173,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
}
public override async Task<OrderListModel> PrepareOrderListModelAsync(OrderSearchModel searchModel)
=>await base.PrepareOrderListModelAsync(searchModel);
=> await base.PrepareOrderListModelAsync(searchModel);
public async Task<OrderListModelExtended> PrepareOrderListModelExtendedAsync(OrderSearchModel searchModel)
{
@ -182,6 +186,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
PropertyHelper.CopyPublicValueTypeProperties(orderModel, orderModelExtended);
orderModelExtended.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(orderModel);
orderModelExtended.DateOfReceipt = await GetPickupDateTimeAsync(orderModel);
Console.WriteLine(orderModelExtended.Id);
extendedRows.Add(orderModelExtended);
@ -210,7 +215,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
return await Task.FromResult(false);
}
private async Task<DateTime?> GetPickupDateTimeAsync(OrderModel order)
{
DateTime? dateTime = DateTime.MinValue;
var fullOrder = await _orderService.GetOrderByIdAsync(order.Id);
if (fullOrder != null)
{
dateTime = await _genericAttributeService.GetAttributeAsync<DateTime>(fullOrder, nameof(IOrderDto.DateOfReceipt));
if(dateTime == DateTime.MinValue || !dateTime.HasValue)
{
dateTime = null;
}
}
return dateTime;
}
}
}

View File

@ -68,7 +68,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
};
await _settingService.SaveSettingAsync(settings);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Shipment", "EN");
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Shipment", "HU");
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Szállítmányok", "HU");
await base.InstallAsync();
}
@ -84,7 +84,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
public Task<IList<string>> GetWidgetZonesAsync()
{
return Task.FromResult<IList<string>>(new List<string> { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock });
return Task.FromResult<IList<string>>(new List<string> { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock, AdminWidgetZones.OrderDetailsBlock });
}
//public string GetWidgetViewComponentName(string widgetZone)
@ -138,6 +138,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
{
return zones.Any(widgetZone.Equals) ? typeof(ProductAttributesViewComponent) : null;
}
else if (widgetZone == AdminWidgetZones.OrderDetailsBlock)
{
return zones.Any(widgetZone.Equals) ? typeof(OrderAttributesViewComponent) : null;
}
}
return null;

View File

@ -111,6 +111,26 @@ public class RouteProvider : IRouteProvider
name: "Plugin.FruitBank.Admin.ManagementPage.GetPartners",
pattern: "Admin/ManagementPage/GetPartners",
defaults: new { controller = "ManagementPage", action = "GetPartners", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.ManagementPage.LoadChildGrid",
pattern: "Admin/ManagementPage/LoadChildGrid",
defaults: new { controller = "ManagementPage", action = "LoadChildGrid", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Products.List",
pattern: "Admin/Product/List",
defaults: new { controller = "CustomProduct", action = "List", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Products.ProductList",
pattern: "Admin/Product/ProductList",
defaults: new { controller = "CustomProduct", action = "ProductList", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Orders.SaveOrderAttributes",
pattern: "Admin/CustomOrder/SaveOrderAttributes",
defaults: new { controller = "CustomOrder", action = "SaveOrderAttributes", area = AreaNames.ADMIN });
}
/// <summary>

View File

@ -0,0 +1,18 @@
using FruitBank.Common.Interfaces;
using Nop.Web.Framework.Models;
using Nop.Web.Framework.Mvc.ModelBinding;
namespace Nop.Plugin.Misc.FruitBankPlugin.Models
{
public record OrderAttributesModel : BaseNopModel, IMeasurable
{
public int OrderId { get; set; }
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.IsMeasurable")]
public bool IsMeasurable { get; set; }
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.DateOfReceipt")]
public DateTime? DateOfReceipt { get; set; }
}
}

View File

@ -5,6 +5,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models
public partial record OrderModelExtended : OrderModel
{
public bool IsMeasurable { get; set; }
public DateTime? DateOfReceipt { get; set; }
}
}

View File

@ -7,6 +7,9 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Services\InnvoiceApiService.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="logo.jpg" />
@ -28,7 +31,10 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="PdfPig" Version="0.1.11" />
<PackageReference Include="PdfPig.Rendering.Skia" Version="0.1.11.5" />
<PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="Tesseract" Version="5.2.0" />
<PackageReference Include="TesseractOCR" Version="5.5.1" />
</ItemGroup>
<ItemGroup>
@ -37,6 +43,11 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Areas\Admin\Views\Product\List.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Areas\Admin\Views\_ViewStart.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
@ -146,7 +157,10 @@
</ItemGroup>
<ItemGroup>
<None Update="Areas\Admin\Components\_DocumentsGridPartial.cshtml">
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Order\TestGridComponent.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Order\ShippingDocumentGridComponent.cshtml">
@ -605,6 +619,9 @@
<None Update="Views\ProductAIListWidget.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\OrderAttributes.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\ProductAttributes.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
@ -620,6 +637,10 @@
<Content Include="$(OutDir)\System.ServiceModel.Primitives.dll" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<None Include="Services\InnvoiceApiService.cs" />
</ItemGroup>
<!-- This target execute after "Build" target -->
<Target Name="NopTarget" AfterTargets="Build">
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />

View File

@ -1,14 +1,6 @@
using Azure;
using Nop.Core;
using Nop.Core;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Stores;
using Nop.Plugin.Misc.FruitBankPlugin.Helpers;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
@ -37,16 +29,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return response;
}
//public async Task<string> GetOpenAIPDFAnalyzisFromText(string pdfText, string userQuestion)
//{
// string systemMessage = $"You are a helpful assistant of a webshop, you work in the administration area, with the ADMIN user. The ADMIN user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
// var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
// var fixedResponse = TextHelper.FixJsonWithoutAI(response);
// return fixedResponse;
//}
public async Task<string> GetOpenAIPDFAnalysisFromText(string pdfText, string userQuestion)
{
string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
@ -57,15 +39,5 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return fixedResponse;
}
//public async Task<string> GetOpenAIPartnerInfoFromText(string pdfText, string userQuestion)
//{
// string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
// var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
// var fixedResponse = TextHelper.FixJsonWithoutAI(response);
// return fixedResponse;
//}
}
}

View File

@ -0,0 +1,496 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml.Linq;
using System.Linq;
using System.Text;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
/// <summary>
/// Service for interacting with InnVoice Invoice API
/// API Documentation: https://help.innvoice.hu/hc/hu/articles/360003142839
/// </summary>
public class InnVoiceApiService
{
private readonly HttpClient _httpClient;
private readonly string _companyName;
private readonly string _username;
private readonly string _password;
private readonly string _baseUrl;
public InnVoiceApiService(string companyName, string username, string password, string baseUrl = "https://api.innvoice.hu")
{
_companyName = companyName ?? throw new ArgumentNullException(nameof(companyName));
_username = username ?? throw new ArgumentNullException(nameof(username));
_password = password ?? throw new ArgumentNullException(nameof(password));
_baseUrl = baseUrl.TrimEnd('/');
_httpClient = new HttpClient();
SetupAuthentication();
}
private void SetupAuthentication()
{
var authToken = Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes($"{_username}:{_password}")
);
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken);
}
/// <summary>
/// Get all invoices
/// Rate limit: 20 times per hour without ID parameter
/// </summary>
public async Task<List<Invoice>> GetAllInvoicesAsync()
{
var url = $"{_baseUrl}/{_companyName}/invoice";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoice by internal table ID
/// </summary>
public async Task<Invoice> GetInvoiceByIdAsync(int tableId)
{
var url = $"{_baseUrl}/{_companyName}/invoice/id/{tableId}";
var invoices = await GetInvoicesFromUrlAsync(url);
return invoices.Count > 0 ? invoices[0] : null;
}
/// <summary>
/// Get invoice by invoice number
/// </summary>
public async Task<List<Invoice>> GetInvoiceByNumberAsync(string invoiceNumber)
{
var url = $"{_baseUrl}/{_companyName}/invoice/invoicenumber/{Uri.EscapeDataString(invoiceNumber)}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by creation date
/// Format: YYYYMMDD
/// </summary>
public async Task<List<Invoice>> GetInvoicesByCreationDateAsync(DateTime date)
{
var dateStr = date.ToString("yyyyMMdd");
var url = $"{_baseUrl}/{_companyName}/invoice/created/{dateStr}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by fulfillment date
/// Format: YYYYMMDD
/// </summary>
public async Task<List<Invoice>> GetInvoicesByFulfillmentDateAsync(DateTime date)
{
var dateStr = date.ToString("yyyyMMdd");
var url = $"{_baseUrl}/{_companyName}/invoice/fulfillment/{dateStr}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by due date
/// Format: YYYYMMDD
/// </summary>
public async Task<List<Invoice>> GetInvoicesByDueDateAsync(DateTime date)
{
var dateStr = date.ToString("yyyyMMdd");
var url = $"{_baseUrl}/{_companyName}/invoice/duedate/{dateStr}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by payment date
/// Format: YYYYMMDD
/// </summary>
public async Task<List<Invoice>> GetInvoicesByPaymentDateAsync(DateTime date)
{
var dateStr = date.ToString("yyyyMMdd");
var url = $"{_baseUrl}/{_companyName}/invoice/paymentdate/{dateStr}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by customer tax number
/// </summary>
public async Task<List<Invoice>> GetInvoicesByCustomerTaxNumberAsync(string taxNumber)
{
var url = $"{_baseUrl}/{_companyName}/invoice/taxnumber/{Uri.EscapeDataString(taxNumber)}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices modified since a specific timestamp
/// Format: YYYYMMDDHHmmss (year, month, day, hour, minute, second)
/// Recommended for tracking changes every 10 minutes
/// Rate limit: Full queries or queries older than current month limited to 10 times per 30 days
/// Recommended: Only use current month dates
/// </summary>
public async Task<List<Invoice>> GetInvoicesByUpdateTimeAsync(DateTime updateTime)
{
var timeStr = updateTime.ToString("yyyyMMddHHmmss");
var url = $"{_baseUrl}/{_companyName}/invoice/updatedtime/{timeStr}";
return await GetInvoicesFromUrlAsync(url);
}
private async Task<List<Invoice>> GetInvoicesFromUrlAsync(string url)
{
try
{
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var invoices = JsonSerializer.Deserialize<List<Invoice>>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return invoices ?? new List<Invoice>();
}
catch (HttpRequestException ex)
{
throw new InnVoiceApiException($"Error calling InnVoice API: {ex.Message}", ex);
}
catch (JsonException ex)
{
throw new InnVoiceApiException($"Error parsing API response: {ex.Message}", ex);
}
}
}
// Models
public class Invoice
{
[JsonPropertyName("TABLE_ID")]
public int TableId { get; set; }
[JsonPropertyName("InvoiceNumber")]
public string InvoiceNumber { get; set; }
[JsonPropertyName("Created")]
public string Created { get; set; }
[JsonPropertyName("Fulfillment")]
public string Fulfillment { get; set; }
[JsonPropertyName("DueDate")]
public string DueDate { get; set; }
[JsonPropertyName("PaymentDate")]
public string PaymentDate { get; set; }
[JsonPropertyName("CustomerName")]
public string CustomerName { get; set; }
[JsonPropertyName("CustomerTaxNumber")]
public string CustomerTaxNumber { get; set; }
[JsonPropertyName("CustomerAddress")]
public string CustomerAddress { get; set; }
[JsonPropertyName("TotalNet")]
public decimal TotalNet { get; set; }
[JsonPropertyName("TotalGross")]
public decimal TotalGross { get; set; }
[JsonPropertyName("Currency")]
public string Currency { get; set; }
[JsonPropertyName("Status")]
public string Status { get; set; }
[JsonPropertyName("InvoiceType")]
public string InvoiceType { get; set; }
[JsonPropertyName("PaymentMethod")]
public string PaymentMethod { get; set; }
// Add more properties as needed based on actual API response
}
public class InnVoiceApiException : Exception
{
public InnVoiceApiException(string message) : base(message) { }
public InnVoiceApiException(string message, Exception innerException) : base(message, innerException) { }
}
// Invoice Creation Models
public class InvoiceCreateRequest
{
public int VevoID { get; set; } = 0;
public string VevoNev { get; set; }
public string VevoIrsz { get; set; }
public string VevoOrszag { get; set; }
public string VevoTelep { get; set; }
public string VevoUtcaHsz { get; set; }
public string VevoEPNev { get; set; }
public string VevoEPKod { get; set; }
public string SzallNev { get; set; }
public string SzallIrsz { get; set; }
public string SzallTelep { get; set; }
public string SzallUtcaHsz { get; set; }
public string SzallOrszag { get; set; }
public int SzamlatombID { get; set; }
public DateTime SzamlaKelte { get; set; }
public DateTime TeljesitesKelte { get; set; }
public DateTime Hatarido { get; set; }
public string Devizanem { get; set; }
public string FizetesiMod { get; set; }
public string Megjegyzes { get; set; }
public string Nyelv1 { get; set; }
public string Nyelv2 { get; set; }
public decimal? Arfolyam { get; set; }
public string ArfolyamDeviza { get; set; }
public bool Fizetve { get; set; }
public bool Eszamla { get; set; }
public string VevoAdoszam { get; set; }
public string VevoCsAdoszam { get; set; }
public string Telefon { get; set; }
public string Email { get; set; }
public string MegrendelesSzamStr { get; set; }
public string MegrendelesIdopontStr { get; set; }
public bool Felretett { get; set; }
public bool Proforma { get; set; }
public bool AutomatikusAr { get; set; }
public bool Eloleg { get; set; }
public bool Sendmail { get; set; }
public string MailSubject { get; set; }
public string MailBody { get; set; }
public string Eredetiszamla { get; set; }
public List<InvoiceItem> Items { get; set; } = new List<InvoiceItem>();
public void AddItem(InvoiceItem item)
{
Items.Add(item);
}
public string ToXml()
{
var invoices = new XElement("invoices");
var invoice = new XElement("invoice");
if (VevoID > 0)
invoice.Add(new XElement("VevoID", new XCData(VevoID.ToString())));
invoice.Add(new XElement("VevoNev", new XCData(VevoNev ?? "")));
invoice.Add(new XElement("VevoIrsz", new XCData(VevoIrsz ?? "")));
invoice.Add(new XElement("VevoTelep", new XCData(VevoTelep ?? "")));
invoice.Add(new XElement("VevoOrszag", new XCData(VevoOrszag ?? "")));
invoice.Add(new XElement("VevoUtcaHsz", new XCData(VevoUtcaHsz ?? "")));
if (!string.IsNullOrEmpty(VevoEPNev))
invoice.Add(new XElement("VevoEPNev", new XCData(VevoEPNev)));
if (!string.IsNullOrEmpty(VevoEPKod))
invoice.Add(new XElement("VevoEPKod", new XCData(VevoEPKod)));
if (!string.IsNullOrEmpty(SzallNev))
invoice.Add(new XElement("SzallNev", new XCData(SzallNev)));
if (!string.IsNullOrEmpty(SzallIrsz))
invoice.Add(new XElement("SzallIrsz", new XCData(SzallIrsz)));
if (!string.IsNullOrEmpty(SzallTelep))
invoice.Add(new XElement("SzallTelep", new XCData(SzallTelep)));
if (!string.IsNullOrEmpty(SzallUtcaHsz))
invoice.Add(new XElement("SzallUtcaHsz", new XCData(SzallUtcaHsz)));
if (!string.IsNullOrEmpty(SzallOrszag))
invoice.Add(new XElement("SzallOrszag", new XCData(SzallOrszag)));
invoice.Add(new XElement("SzamlatombID", new XCData(SzamlatombID.ToString())));
invoice.Add(new XElement("SzamlaKelte", new XCData(SzamlaKelte.ToString("yyyy.MM.dd."))));
invoice.Add(new XElement("TeljesitesKelte", new XCData(TeljesitesKelte.ToString("yyyy.MM.dd."))));
invoice.Add(new XElement("Hatarido", new XCData(Hatarido.ToString("yyyy.MM.dd."))));
invoice.Add(new XElement("Devizanem", new XCData(Devizanem ?? "")));
invoice.Add(new XElement("FizetesiMod", new XCData(FizetesiMod ?? "")));
if (!string.IsNullOrEmpty(Megjegyzes))
invoice.Add(new XElement("Megjegyzes", new XCData(Megjegyzes)));
if (!string.IsNullOrEmpty(Nyelv1))
invoice.Add(new XElement("Nyelv1", new XCData(Nyelv1)));
if (!string.IsNullOrEmpty(Nyelv2))
invoice.Add(new XElement("Nyelv2", new XCData(Nyelv2)));
if (Arfolyam.HasValue)
invoice.Add(new XElement("Arfolyam", new XCData(Arfolyam.Value.ToString())));
if (!string.IsNullOrEmpty(ArfolyamDeviza))
invoice.Add(new XElement("ArfolyamDeviza", new XCData(ArfolyamDeviza)));
invoice.Add(new XElement("Fizetve", Fizetve ? "1" : "0"));
invoice.Add(new XElement("Eszamla", Eszamla ? "1" : "0"));
if (!string.IsNullOrEmpty(VevoAdoszam))
invoice.Add(new XElement("VevoAdoszam", new XCData(VevoAdoszam)));
if (!string.IsNullOrEmpty(VevoCsAdoszam))
invoice.Add(new XElement("VevoCsAdoszam", new XCData(VevoCsAdoszam)));
if (!string.IsNullOrEmpty(Telefon))
invoice.Add(new XElement("Telefon", new XCData(Telefon)));
if (!string.IsNullOrEmpty(Email))
invoice.Add(new XElement("Email", new XCData(Email)));
if (!string.IsNullOrEmpty(MegrendelesSzamStr))
invoice.Add(new XElement("MegrendelesSzamStr", new XCData(MegrendelesSzamStr)));
if (!string.IsNullOrEmpty(MegrendelesIdopontStr))
invoice.Add(new XElement("MegrendelesIdopontStr", new XCData(MegrendelesIdopontStr)));
invoice.Add(new XElement("Felretett", Felretett ? "1" : "0"));
invoice.Add(new XElement("Proforma", Proforma ? "1" : "0"));
if (AutomatikusAr)
invoice.Add(new XElement("AutomatikusAr", "1"));
if (Eloleg)
invoice.Add(new XElement("Eloleg", "1"));
if (Sendmail)
{
invoice.Add(new XElement("Sendmail", "1"));
if (!string.IsNullOrEmpty(MailSubject))
invoice.Add(new XElement("MailSubject", new XCData(MailSubject)));
if (!string.IsNullOrEmpty(MailBody))
invoice.Add(new XElement("MailBody", new XCData(MailBody)));
}
if (!string.IsNullOrEmpty(Eredetiszamla))
invoice.Add(new XElement("Eredetiszamla", new XCData(Eredetiszamla)));
// Add items
foreach (var item in Items)
{
var tetel = new XElement("tetel");
tetel.Add(new XElement("TetelNev", new XCData(item.TetelNev ?? "")));
tetel.Add(new XElement("AfaSzoveg", item.AfaSzoveg ?? ""));
tetel.Add(new XElement("Brutto", item.Brutto ? "1" : "0"));
tetel.Add(new XElement("EgysegAr", item.EgysegAr.ToString()));
tetel.Add(new XElement("Mennyiseg", item.Mennyiseg.ToString()));
tetel.Add(new XElement("MennyisegEgyseg", new XCData(item.MennyisegEgyseg ?? "")));
if (item.KedvezmenyOsszeg.HasValue)
tetel.Add(new XElement("KedvezmenyOsszeg", item.KedvezmenyOsszeg.Value.ToString()));
if (item.TermekID.HasValue)
tetel.Add(new XElement("TermekID", item.TermekID.Value.ToString()));
if (!string.IsNullOrEmpty(item.Megjegyzes))
tetel.Add(new XElement("Megjegyzes", new XCData(item.Megjegyzes)));
if (!string.IsNullOrEmpty(item.CikkSzam))
tetel.Add(new XElement("CikkSzam", new XCData(item.CikkSzam)));
if (!string.IsNullOrEmpty(item.VTSZSZJ))
tetel.Add(new XElement("VTSZSZJ", new XCData(item.VTSZSZJ)));
if (item.ElolegSzamlaTABLE_ID.HasValue)
tetel.Add(new XElement("ElolegSzamlaTABLE_ID", item.ElolegSzamlaTABLE_ID.Value.ToString()));
if (!string.IsNullOrEmpty(item.ElolegSzamlaSorszam))
tetel.Add(new XElement("ElolegSzamlaSorszam", new XCData(item.ElolegSzamlaSorszam)));
invoice.Add(tetel);
}
invoices.Add(invoice);
return new XDeclaration("1.0", "UTF-8", null).ToString() + "\n" + invoices.ToString();
}
}
public class InvoiceItem
{
public string TetelNev { get; set; }
public string AfaSzoveg { get; set; }
public bool Brutto { get; set; }
public decimal EgysegAr { get; set; }
public decimal Mennyiseg { get; set; }
public string MennyisegEgyseg { get; set; }
public decimal? KedvezmenyOsszeg { get; set; }
public int? TermekID { get; set; }
public string Megjegyzes { get; set; }
public string CikkSzam { get; set; }
public string VTSZSZJ { get; set; }
public int? ElolegSzamlaTABLE_ID { get; set; }
public string ElolegSzamlaSorszam { get; set; }
}
public class InvoiceCreateResponse
{
public string ErrorCode { get; set; }
public string Message { get; set; }
public int? TableId { get; set; }
public int? VevoID { get; set; }
public string TechId { get; set; }
public string Sorszam { get; set; }
public string PrintUrl { get; set; }
public bool IsSuccess => ErrorCode == "200";
public static InvoiceCreateResponse FromXml(string xml)
{
var doc = XDocument.Parse(xml);
var invoice = doc.Descendants("invoice").FirstOrDefault();
if (invoice == null)
{
throw new InnVoiceApiException("Invalid XML response format");
}
return new InvoiceCreateResponse
{
ErrorCode = invoice.Element("error")?.Value,
Message = invoice.Element("message")?.Value?.Trim(),
TableId = int.TryParse(invoice.Element("TABLE_ID")?.Value?.Trim(), out var tid) ? tid : (int?)null,
VevoID = int.TryParse(invoice.Element("VevoID")?.Value?.Trim(), out var vid) ? vid : (int?)null,
TechId = invoice.Element("techid")?.Value?.Trim(),
Sorszam = invoice.Element("Sorszam")?.Value?.Trim(),
PrintUrl = invoice.Element("PrintUrl")?.Value?.Trim()
};
}
}
/// <summary>
/// Create a new invoice
/// </summary>
public async Task<InvoiceCreateResponse> CreateInvoiceAsync(InvoiceCreateRequest request)
{
var url = $"{_baseUrl}/{_companyName}/invoice";
var xml = request.ToXml();
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("data", xml)
});
try
{
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return InvoiceCreateResponse.FromXml(responseContent);
}
catch (HttpRequestException ex)
{
throw new InnVoiceApiException($"Error creating invoice: {ex.Message}", ex);
}
}
/// <summary>
/// Update an existing invoice
/// </summary>
public async Task<InvoiceCreateResponse> UpdateInvoiceAsync(int tableId, InvoiceCreateRequest request)
{
// Set the VevoID if updating customer information
var url = $"{_baseUrl}/{_companyName}/invoice";
var xml = request.ToXml();
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("data", xml),
new KeyValuePair<string, string>("id", tableId.ToString())
});
try
{
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return InvoiceCreateResponse.FromXml(responseContent);
}
catch (HttpRequestException ex)
{
throw new InnVoiceApiException($"Error updating invoice: {ex.Message}", ex);
}
}

View File

@ -21,6 +21,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
private const string OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations";
private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files";
private const string BaseUrl = "https://api.openai.com/v1";
private string? _assistantId;
private string? _vectorStoreId;
public OpenAIApiService(ISettingService settingService, HttpClient httpClient)
{
@ -286,66 +290,371 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
#endregion
#region === PDF ANALYSIS (NEW) ===
public async Task<string?> AnalyzePdfAsync(string filePath, string userPrompt)
{
// Step 1: Upload PDF
using var form = new MultipartFormDataContent();
using var fileStream = File.OpenRead(filePath);
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(fileContent, "file", Path.GetFileName(filePath));
form.Add(new StringContent("assistants"), "purpose");
var uploadResponse = await _httpClient.PostAsync(OpenAiFileEndpoint, form);
if (!uploadResponse.IsSuccessStatusCode)
private async Task EnsureAssistantAndVectorStoreAsync()
{
var error = await uploadResponse.Content.ReadAsStringAsync();
throw new Exception($"File upload failed: {error}");
// Find or create vector store
if (_vectorStoreId == null)
{
_vectorStoreId = await FindOrCreateVectorStoreAsync("pdf-analysis-store");
}
using var uploadJson = await JsonDocument.ParseAsync(await uploadResponse.Content.ReadAsStreamAsync());
var fileId = uploadJson.RootElement.GetProperty("id").GetString();
// Find or create assistant
if (_assistantId == null)
{
_assistantId = await FindOrCreateAssistantAsync("PDF and Image Analyzer Assistant");
}
}
// Step 2: Ask model with file reference
var requestBody = new
//TEMPORARY: Cleanup all assistants (for testing purposes) - A.
public async Task CleanupAllAssistantsAsync()
{
model = "gpt-4.1", // must support file_search
messages = new[]
Console.WriteLine("Cleaning up all existing assistants...");
var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants");
listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
var response = await _httpClient.SendAsync(listRequest);
if (response.IsSuccessStatusCode)
{
new { role = "system", content = "You are an assistant that analyzes uploaded PDF files." },
new { role = "user", content = userPrompt }
},
tools = new[]
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var assistants = json.RootElement.GetProperty("data");
foreach (var assistant in assistants.EnumerateArray())
{
new { type = "file_search" }
},
var id = assistant.GetProperty("id").GetString();
var name = assistant.GetProperty("name").GetString();
var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"{BaseUrl}/assistants/{id}");
deleteRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
await _httpClient.SendAsync(deleteRequest);
Console.WriteLine($"Deleted assistant: {name} ({id})");
}
Console.WriteLine("Cleanup complete!");
}
// Reset local cache
_assistantId = null;
}
public async Task<string?> AnalyzePdfAsync(Stream file, string fileName, string userPrompt)
{
await EnsureAssistantAndVectorStoreAsync();
var fileId = await UploadFileAsync(file, fileName);
var isImage = IsImageFile(fileName);
if (!isImage)
{
await AttachFileToVectorStoreAsync(fileId);
}
var threadId = await CreateThreadAsync();
if (isImage)
{
await AddUserMessageWithImageAsync(threadId, userPrompt, fileId);
}
else
{
await AddUserMessageAsync(threadId, userPrompt);
}
var runId = await CreateRunAsync(threadId);
await WaitForRunCompletionAsync(threadId, runId);
return await GetAssistantResponseAsync(threadId);
}
private bool IsImageFile(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".gif" || extension == ".webp";
}
private async Task<string> UploadFileAsync(Stream file, string fileName)
{
using var form = new MultipartFormDataContent();
var fileContent = new StreamContent(file);
// Determine MIME type based on file extension
var extension = Path.GetExtension(fileName).ToLowerInvariant();
fileContent.Headers.ContentType = extension switch
{
".pdf" => new MediaTypeHeaderValue("application/pdf"),
".jpg" or ".jpeg" => new MediaTypeHeaderValue("image/jpeg"),
".png" => new MediaTypeHeaderValue("image/png"),
".gif" => new MediaTypeHeaderValue("image/gif"),
".webp" => new MediaTypeHeaderValue("image/webp"),
_ => new MediaTypeHeaderValue("application/octet-stream")
};
form.Add(fileContent, "file", fileName);
form.Add(new StringContent("assistants"), "purpose");
var response = await _httpClient.PostAsync($"{BaseUrl}/files", form);
await EnsureSuccessAsync(response, "upload file");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
return json.RootElement.GetProperty("id").GetString()!;
}
private async Task AttachFileToVectorStoreAsync(string fileId)
{
var body = new { file_id = fileId };
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/vector_stores/{_vectorStoreId}/files",
body
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "attach file to vector store");
}
private async Task<string> CreateThreadAsync()
{
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/threads",
new { }
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "create thread");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
return json.RootElement.GetProperty("id").GetString()!;
}
private async Task AddUserMessageAsync(string threadId, string userPrompt)
{
var body = new
{
role = "user",
content = userPrompt
};
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/threads/{threadId}/messages",
body
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "add user message");
}
private async Task AddUserMessageWithImageAsync(string threadId, string userPrompt, string fileId)
{
var body = new
{
role = "user",
content = new object[]
{
new { type = "text", text = userPrompt },
new { type = "image_file", image_file = new { file_id = fileId } }
}
};
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/threads/{threadId}/messages",
body
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "add user message with image");
}
private async Task<string> CreateRunAsync(string threadId)
{
var body = new
{
assistant_id = _assistantId,
tool_resources = new
{
file_search = new
{
vector_store_ids = new string[] { fileId! }
vector_store_ids = new[] { _vectorStoreId }
}
}
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/threads/{threadId}/runs",
body
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "create run");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
return json.RootElement.GetProperty("id").GetString()!;
}
private async Task WaitForRunCompletionAsync(string threadId, string runId)
{
const int pollIntervalMs = 1000;
const int maxAttempts = 60; // 1 minute timeout
int attempts = 0;
while (attempts < maxAttempts)
{
var request = CreateAssistantRequest(
HttpMethod.Get,
$"{BaseUrl}/threads/{threadId}/runs/{runId}"
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "check run status");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var status = json.RootElement.GetProperty("status").GetString()!;
if (status == "completed")
return;
if (status != "in_progress" && status != "queued")
throw new Exception($"Run failed with status: {status}");
await Task.Delay(pollIntervalMs);
attempts++;
}
throw new TimeoutException("Run did not complete within the expected time");
}
private async Task<string?> GetAssistantResponseAsync(string threadId)
{
var request = CreateAssistantRequest(
HttpMethod.Get,
$"{BaseUrl}/threads/{threadId}/messages"
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "retrieve messages");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var messages = json.RootElement.GetProperty("data");
if (messages.GetArrayLength() == 0)
return "No response";
var firstMessage = messages[0]
.GetProperty("content")[0]
.GetProperty("text")
.GetProperty("value")
.GetString();
return firstMessage ?? "No response";
}
private HttpRequestMessage CreateAssistantRequest(HttpMethod method, string url, object? body = null)
{
var request = new HttpRequestMessage(method, url);
request.Headers.Add("OpenAI-Beta", "assistants=v2");
if (body != null)
{
var json = JsonSerializer.Serialize(body, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
var chatResponse = await _httpClient.PostAsync(OpenAiEndpoint, requestContent);
chatResponse.EnsureSuccessStatusCode();
using var responseJson = await JsonDocument.ParseAsync(await chatResponse.Content.ReadAsStreamAsync());
var result = responseJson.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
return result ?? "No response from model";
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
return request;
}
private async Task EnsureSuccessAsync(HttpResponseMessage response, string operation)
{
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Error Status: {response.StatusCode}");
Console.WriteLine($"Error Body: {errorBody}");
throw new Exception($"Failed to {operation}: {errorBody}");
}
}
private async Task<string> FindOrCreateVectorStoreAsync(string name)
{
// List existing vector stores
var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/vector_stores");
listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
var response = await _httpClient.SendAsync(listRequest);
if (response.IsSuccessStatusCode)
{
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var stores = json.RootElement.GetProperty("data");
foreach (var store in stores.EnumerateArray())
{
if (store.GetProperty("name").GetString() == name)
{
return store.GetProperty("id").GetString()!;
}
}
}
// Create new if not found
var createBody = new { name = name };
var createRequest = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/vector_stores", createBody);
var createResponse = await _httpClient.SendAsync(createRequest);
await EnsureSuccessAsync(createResponse, "create vector store");
using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync());
return createJson.RootElement.GetProperty("id").GetString()!;
}
private async Task<string> FindOrCreateAssistantAsync(string name)
{
// List existing assistants
var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants");
listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
var response = await _httpClient.SendAsync(listRequest);
if (response.IsSuccessStatusCode)
{
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var assistants = json.RootElement.GetProperty("data");
foreach (var assistant in assistants.EnumerateArray())
{
if (assistant.GetProperty("name").GetString() == name)
{
return assistant.GetProperty("id").GetString()!;
}
}
}
// Create new if not found
var assistantBody = new
{
name = name,
instructions = "You are an assistant that analyzes uploaded files. When you receive an image, analyze and describe what you see in the image in detail. When you receive a PDF or text document, use the file_search tool to find and analyze relevant information. Always respond directly to the user's question about the file they uploaded.",
model = "gpt-4o",
tools = new[] { new { type = "file_search" } }
};
var request = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/assistants", assistantBody);
var createResponse = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(createResponse, "create assistant");
using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync());
return createJson.RootElement.GetProperty("id").GetString()!;
}
#endregion
}
}

View File

@ -0,0 +1,61 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Models.OrderAttributesModel
<div class="card card-default">
<div class="card-header">
<i class="fas fa-tags"></i>
Custom Order Attributes
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="IsMeasurable" />
</div>
<div class="col-md-9">
<nop-editor asp-for="IsMeasurable" />
<span asp-validation-for="IsMeasurable"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="DateOfReceipt" />
</div>
<div class="col-md-9">
<nop-editor asp-for="DateOfReceipt" />
<span asp-validation-for="DateOfReceipt"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-12 text-right">
<button type="button" id="saveAttributesBtn" class="btn btn-primary">
<i class="fa fa-save"></i> Save Attributes
</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$("#saveAttributesBtn").click(function () {
$.ajax({
type: "POST",
url: "@Url.Action("SaveOrderAttributes", "CustomOrder")",
data: {
orderId: "@Model.OrderId",
isMeasurable: $("#@Html.IdFor(m => m.IsMeasurable)").is(":checked"),
pickupDateTimeUtc: $("#@Html.IdFor(m => m.DateOfReceipt)").val(),
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
},
success: function () {
alert("Attributes saved successfully");
},
error: function () {
alert("Error saving attributes");
}
});
});
});
</script>

View File

@ -1,4 +1,4 @@
@* File: Plugins/Nop.Plugin.YourCompany.ProductAttributes/Views/ProductCustomAttributes.cshtml *@

@model Nop.Plugin.Misc.FruitBankPlugin.Models.ProductAttributesModel