From 155702b653612a6f7a2f9b52434720f8992db715 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 10 Oct 2025 02:26:27 +0200 Subject: [PATCH 1/3] AI progress --- .../Controllers/ManagementPageController.cs | 339 +++++++++++++++--- .../Admin/Controllers/ShippingController.cs | 2 +- .../Areas/Admin/Views/Order/List.cshtml | 2 +- .../ShippingDocumentGridComponent.cshtml | 5 +- .../Domains/DataLayer/PartnerDbTable.cs | 4 + .../Factories/CustomOrderModelFactory.cs | 2 +- .../Models/OrderModelExtended.cs | 2 +- .../Services/AICalculationService.cs | 22 +- .../Services/EventConsumer.cs | 3 +- .../Services/OrderMeasurementService.cs | 3 +- 10 files changed, 322 insertions(+), 62 deletions(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs index ce0c8f3..a8138a3 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs @@ -1,4 +1,6 @@ -using FruitBank.Common.Entities; +using DevExpress.Pdf.Native; +using FruitBank.Common; +using FruitBank.Common.Entities; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -11,11 +13,12 @@ using Nop.Web.Areas.Admin.Controllers; using Nop.Web.Framework; using Nop.Web.Framework.Mvc.Filters; using System.Text; +using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor; namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { [Area(AreaNames.ADMIN)] - [AuthorizeAdmin] + [AuthorizeAdmin] public class ManagementPageController : BaseAdminController { private readonly IPermissionService _permissionService; @@ -40,7 +43,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers testGridModel2.GridName = "Orders"; testGridModel2.ViewComponentName = "ShippingDocumentGridComponent"; testPageModel.Grids.Add(testGridModel2); - + var testGridModel = new TestGridModel(); testGridModel.GridName = "Shipping"; testGridModel.ViewComponentName = "ShippingGridComponent"; @@ -60,7 +63,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return AccessDeniedView(); - + // Mock data for now var model = _dbContext.Shippings.GetAll(true).OrderByDescending(s => s.Created).ToList(); var valami = model; @@ -92,7 +95,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return Json(model); } - + [HttpGet] public async Task GetPartners() { @@ -135,34 +138,144 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers [HttpPost] [RequestSizeLimit(10485760)] // 10MB [RequestFormLimits(MultipartBodyLengthLimit = 10485760)] - public async Task UploadFile(UploadModel model) + public async Task UploadFile(List files, int shippingDocumentId, int? partnerId) { - var files = model.Files; - var shippingDocumentId = model.ShippingDocumentId; - try + //checks + // - files exist + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, errorMessage = "Access denied" }); + + // - permissions + if (files == null || files.Count == 0) + return Json(new { success = false, errorMessage = "No file selected" }); + + // - do we have partnerId - if so, we already have at least one doecument uploaded earlier + if (partnerId.HasValue) { - if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) - return Json(new { success = false, errorMessage = "Access denied" }); + Console.WriteLine($"Associated with Partner ID: {partnerId.Value}"); - if (files == null || files.Count == 0) - return Json(new { success = false, errorMessage = "No file selected" }); + //let's get the partner + var partner = await _dbContext.Partners.GetByIdAsync(partnerId.Value); + } - foreach (var file in files) + var filesList = new List(); + var shippingDocumentToFileList = new List(); + + + //iteratation 1: iterate documents to determine their type by AI + + + 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}"); + + if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) + return Json(new { success = false, errorMessage = "Only PDF files are allowed" }); + + // Validate file size (max 20MB) + if (file.Length > 20 * 1024 * 1024) + return Json(new { success = false, errorMessage = "File size must be less than 10MB" }); + + // - get text extracted from pdf + // Validate file type (PDF only) + if (file.Length > 0 && file.ContentType == "application/pdf") { - // Validate file type (PDF only) - if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) - return Json(new { success = false, errorMessage = "Only PDF files are allowed" }); - // Validate file size (max 10MB) - if (file.Length > 10 * 1024 * 1024) - return Json(new { success = false, errorMessage = "File size must be less than 10MB" }); + try + { + // Open the PDF from the IFormFile's stream directly in memory + using (var stream = file.OpenReadStream()) + using (var pdf = UglyToad.PdfPig.PdfDocument.Open(stream)) + { + // Now you can analyze the PDF content + foreach (var page in pdf.GetPages()) + { + // Extract text from each page + pdfText += ContentOrderTextExtractor.GetText(page); + } + + // 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}"); + } + } + + 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. " + + $"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."; + + //here I can start preparing the file entity + var metaAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), analysisPrompt); + + var extractedMetaData = ParseMetaDataAIResponse(metaAnalyzis); + + if (extractedMetaData.DocumentNumber != null) + + dbFile.RawText = pdfText; + dbFile.FileExtension = "pdf"; + dbFile.FileName = extractedMetaData.DocumentNumber; + + // - 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 } + if (partnerId == null) + { + string partnerAnalysisPrompt = "Extract the partner information from this document, and return them as JSON: name, taxId, certificationNumber, postalCode, country, state, county, city, street. " + + "If you can't find information of any of these, return null value for that field."; + + //here I can start preparing the file entity + var partnerAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), analysisPrompt); + var extractedPartnerData = ParsePartnerDataAIResponse(partnerAnalyzis); + + if (extractedPartnerData.Name != null) + { + Console.WriteLine("AI Analysis Partner Result:"); + Console.WriteLine(extractedPartnerData.Name); + } + + if (extractedPartnerData.TaxId != null) + { + Console.WriteLine(extractedPartnerData.TaxId); + } + + if (extractedPartnerData.Country != null) { + + Console.WriteLine(extractedPartnerData.Country); + } + + if (extractedPartnerData.State != null) + { + Console.WriteLine(extractedPartnerData.State); + } + + } + + // - save the documents to file system - wwwroot/uploads/orders/order-{orderId}/fileId-documentId.pdf + // where documentId is the number or id IN the document + try + { // Create upload directory if it doesn't exist - var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shippingDocuments"); + var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "orders", "order" + shippingDocumentId.ToString(), "documents"); Directory.CreateDirectory(uploadsPath); // Generate unique filename - var fileName = $"{Guid.NewGuid()}_{file.FileName}"; + fileName = $"{Guid.NewGuid()}_{file.FileName}"; var filePath = Path.Combine(uploadsPath, fileName); // Save file @@ -170,23 +283,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { await file.CopyToAsync(stream); } + } + catch (Exception ex) + { + Console.WriteLine($"Error saving file: {ex}"); + //return Json(new { success = false, errorMessage = ex.Message }); + return BadRequest("No files were uploaded."); + } - // Extract text with PdfPig - var pdfText = new StringBuilder(); - using (var pdf = UglyToad.PdfPig.PdfDocument.Open(filePath)) - { - foreach (var page in pdf.GetPages()) - { - pdfText.AppendLine(page.Text); - } - } - // Log extracted text for debugging - Console.WriteLine("Extracted PDF text:"); - Console.WriteLine(pdfText.ToString()); + // - create a list of documents to read information and to save the document's information to DB + // - INSIDE ITERATION: + // - IF SHIPPINGDOCUMENT: read shipping information + // save document information into DB {Id, PartnerId, ShippingId, DocumentIdNumber, ShippingDate, Country, TotalPallets, IsAllMeasured} + // - IF Orderdocument: try reading shipping information and financial information + // save document information into DB {Id, PartnerId, ShippingId, DocumentIdNumber, ShippingDate, Country, TotalPallets, IsAllMeasured} + // - IF Invoice: try reading financial information + // save document information into DB {financial db table not created yet} + // - save the documents to Files Table - { name, extension, type, rawtext } + + try + { // Analyze PDF with AI to extract structured data - var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalyzisFromText( + var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText( pdfText.ToString(), "Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field." ); @@ -224,29 +344,69 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging }; + + + // var savedDocument = await _documentService.InsertDocumentAsync(document); + + // Mock saved document ID + + + //return Json(new + //{ + // success = true, + // document = documentModel + //}); + + } + catch (Exception ex) + { + Console.WriteLine($"Error uploading file: {ex}"); + //return Json(new { success = false, errorMessage = ex.Message }); + return BadRequest("No files were uploaded."); } - // var savedDocument = await _documentService.InsertDocumentAsync(document); - - // Mock saved document ID - - - //return Json(new - //{ - // success = true, - // document = documentModel - //}); - - return Ok($"Files for Shipping Document ID {shippingDocumentId} were uploaded successfully!"); - } - catch (Exception ex) - { - Console.WriteLine($"Error uploading file: {ex}"); - //return Json(new { success = false, errorMessage = ex.Message }); - return BadRequest("No files were uploaded."); } + + + + //iteration 2: iterate documents again + // - determine the items listed in the documents by AI + // try to fill all shippingitems information from the iteration + + + return Ok($"Files for Shipping Document ID {shippingDocumentId} were uploaded successfully!"); } + + //[HttpPost] + //public async Task UploadFile(List files, int shippingDocumentId) + //{ + // if (files == null || files.Count == 0) + // { + // return BadRequest("No files were uploaded."); + // } + + // foreach (var file in files) + // { + // // Here, you would implement your logic to save the file. + // // For example, you can save the file to a specific folder on the server + // // or a cloud storage service like Azure Blob Storage. + // // You would use the 'shippingDocumentId' to associate the file with the correct entity in your database. + + // // Example: Get file details + // var fileName = file.FileName; + // var fileSize = file.Length; + + // // Log or process the file and its associated ID. + // // For demonstration, let's just return a success message. + // Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}"); + // } + + // // Return a success response. The DevExtreme FileUploader expects a 200 OK status. + // return Ok(new { message = "Files uploaded successfully." }); + //} + + private ExtractedDocumentData ParseShippingDocumentAIResponse(string aiResponse) { try @@ -268,6 +428,79 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers } } + private ExtractedDocumentMetaData ParseMetaDataAIResponse(string aiResponse) + { + try + { + // Try to parse as JSON first + var data = System.Text.Json.JsonSerializer.Deserialize( + aiResponse, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true } + ); + return data ?? new ExtractedDocumentMetaData(); + } + catch + { + // If JSON parsing fails, return empty data with the raw response in notes + return new ExtractedDocumentMetaData + { + DocumentNumber = $"Unknown", + DocumentType = "Unknown" + }; + } + } + + private ExtractedPartnerData ParsePartnerDataAIResponse(string aiResponse) + { + try + { + // Try to parse as JSON first + var data = System.Text.Json.JsonSerializer.Deserialize( + aiResponse, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true } + ); + return data ?? new ExtractedPartnerData(); + } + catch + { + // If JSON parsing fails, return empty data with the raw response in notes + return new ExtractedPartnerData + { + Name = "Unknown", + CertificationNumber = "Unknown", + TaxId = "Unknown", + PostalCode = "Unknown", + Country = "Unknown", + State = "Unknown", + County = "Unknown", + City = "Unknown", + Street = "Unknown", + }; + } + } + + private class ExtractedDocumentMetaData + { + public string DocumentNumber { get; set; } + public string DocumentType { get; set; } + + } + + private class ExtractedPartnerData + { + //Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street, + public string Name { get; set; } + public string TaxId { get; set; } + public string CertificationNumber { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string State { get; set; } + public string County { get; set; } + public string City { get; set; } + public string Street { get; set; } + + } + // Helper class for extracted data private class ExtractedDocumentData { diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ShippingController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ShippingController.cs index 2215faf..4e1ee9c 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ShippingController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ShippingController.cs @@ -329,7 +329,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers Console.WriteLine(pdfText.ToString()); // Analyze PDF with AI to extract structured data - var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalyzisFromText( + var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText( pdfText.ToString(), "Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field." ); diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml index ebc6b19..9a7d5c2 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml @@ -340,7 +340,7 @@ Width = "80" } }; - gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.NeedsMeasurement)) + gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable)) { Title = "Needs Measurement", Width = "100", diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml index d9b544d..016abc1 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml @@ -145,7 +145,10 @@ .UploadMode(FileUploadMode.UseForm) ) - + + <% if (data.PartnerId) { %> + + <% } %> @(Html.DevExtreme().Button() .Text("Upload Files") diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDbTable.cs b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDbTable.cs index 63f2fc6..bede242 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDbTable.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/DataLayer/PartnerDbTable.cs @@ -31,4 +31,8 @@ public class PartnerDbTable : MgDbTableBase public Task GetByIdAsync(int id, bool loadRelations) => GetAll(loadRelations).FirstOrDefaultAsync(p => p.Id == id); + + + public Task GetByIdNameAsync(string name, bool loadRelations) + => GetAll(loadRelations).FirstOrDefaultAsync(p => p.Name == name); } \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs index 280c2c5..2601e62 100644 --- a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs +++ b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs @@ -176,7 +176,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories { var extendedOrder = new OrderModelExtended(); CopyModelHelper.CopyPublicProperties(order, extendedOrder); - extendedOrder.NeedsMeasurement = await ShouldMarkAsNeedsMeasurementAsync(order); + extendedOrder.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(order); Console.WriteLine(extendedOrder.Id); extendedRows.Add(extendedOrder); } diff --git a/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs b/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs index afef1c0..5d6c936 100644 --- a/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs +++ b/Nop.Plugin.Misc.AIPlugin/Models/OrderModelExtended.cs @@ -4,7 +4,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models { public partial record OrderModelExtended : OrderModel { - public bool NeedsMeasurement { get; set; } + public bool IsMeasurable { get; set; } } } diff --git a/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs b/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs index d4b5881..e90d7c1 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/AICalculationService.cs @@ -37,9 +37,19 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return response; } - public async Task GetOpenAIPDFAnalyzisFromText(string pdfText, string userQuestion) + //public async Task 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 GetOpenAIPDFAnalysisFromText(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}"; + 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); @@ -47,7 +57,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return fixedResponse; } + //public async Task 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; + //} } } diff --git a/Nop.Plugin.Misc.AIPlugin/Services/EventConsumer.cs b/Nop.Plugin.Misc.AIPlugin/Services/EventConsumer.cs index b4cd883..e2bf14a 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/EventConsumer.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/EventConsumer.cs @@ -3,6 +3,7 @@ using FruitBank.Common.Interfaces; using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Orders; +using Nop.Plugin.Misc.FruitBankPlugin.Models; using Nop.Services.Catalog; using Nop.Services.Common; using Nop.Services.Events; @@ -113,7 +114,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services var store = await _storeContext.GetCurrentStoreAsync(); // itt adjuk hozzá a GenericAttribute flag-et az orderhez await _genericAttributeService.SaveAttributeAsync(order, - "PendingMeasurement", true, store.Id); + nameof(OrderModelExtended.IsMeasurable), true, store.Id); // status pending // paymentstatus pending } diff --git a/Nop.Plugin.Misc.AIPlugin/Services/OrderMeasurementService.cs b/Nop.Plugin.Misc.AIPlugin/Services/OrderMeasurementService.cs index 9268594..c84f94f 100644 --- a/Nop.Plugin.Misc.AIPlugin/Services/OrderMeasurementService.cs +++ b/Nop.Plugin.Misc.AIPlugin/Services/OrderMeasurementService.cs @@ -1,4 +1,5 @@ using Nop.Core.Domain.Orders; +using Nop.Plugin.Misc.FruitBankPlugin.Models; using Nop.Services.Common; namespace Nop.Plugin.Misc.FruitBankPlugin.Services @@ -23,7 +24,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services return false; return await _genericAttributeService.GetAttributeAsync( - order, "PendingMeasurement", order.StoreId); + order, nameof(OrderModelExtended.IsMeasurable), order.StoreId); } } } From 7e7a6e098246c10131e5d4e30e30749de1418000 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 10 Oct 2025 22:59:23 +0200 Subject: [PATCH 2/3] edit and update ismeasurable, netweight, incomingQuantity --- .../Controllers/ManagementPageController.cs | 6 +- .../Admin/Models/Catalog/ProductModel.cs | 21 ++++++ .../ProductAttributesViewComponent.cs | 73 +++++++++++++++++++ .../EventConsumers/FruitBankEventConsumer.cs | 55 +++++++++++++- Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs | 17 ++++- .../Models/ProductAttributesModel.cs | 19 +++++ .../Nop.Plugin.Misc.FruitBankPlugin.csproj | 6 ++ .../Views/ProductAttributes.cshtml | 41 +++++++++++ .../Views/ProductList.cshtml | 54 ++++++++++++++ 9 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/Catalog/ProductModel.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Components/ProductAttributesViewComponent.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Models/ProductAttributesModel.cs create mode 100644 Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml create mode 100644 Nop.Plugin.Misc.AIPlugin/Views/ProductList.cshtml diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs index a8138a3..86a7c7c 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs @@ -234,13 +234,13 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers // - 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 } - if (partnerId == null) + if (partnerId != null) { - string partnerAnalysisPrompt = "Extract the partner information from this document, and return them as JSON: name, taxId, certificationNumber, postalCode, country, state, county, city, street. " + + string partnerAnalysisPrompt = "Extract the sender information from this document, and return them as JSON: name, taxId, certificationNumber, postalCode, country, state, county, city, street. " + "If you can't find information of any of these, return null value for that field."; //here I can start preparing the file entity - var partnerAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), analysisPrompt); + var partnerAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), partnerAnalysisPrompt); var extractedPartnerData = ParsePartnerDataAIResponse(partnerAnalyzis); if (extractedPartnerData.Name != null) diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/Catalog/ProductModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/Catalog/ProductModel.cs new file mode 100644 index 0000000..0e24930 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/Catalog/ProductModel.cs @@ -0,0 +1,21 @@ +using Nop.Web.Areas.Admin.Models.Catalog; +using Nop.Web.Framework.Models; +using Nop.Web.Framework.Mvc.ModelBinding; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.Catalog +{ + public partial record ProductModel : BaseNopEntityModel + { + // ... existing properties ... + + [NopResourceDisplayName("Admin.Catalog.Products.Fields.IsMeasurable")] + public bool IsMeasurable { get; set; } + + // ... rest of your model ... + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Components/ProductAttributesViewComponent.cs b/Nop.Plugin.Misc.AIPlugin/Components/ProductAttributesViewComponent.cs new file mode 100644 index 0000000..06ba9bb --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Components/ProductAttributesViewComponent.cs @@ -0,0 +1,73 @@ +// 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.Plugin.Misc.FruitBankPlugin.Models; +using Nop.Services.Common; +using Nop.Web.Areas.Admin.Models.Catalog; +using Nop.Web.Framework.Components; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Components +{ + [ViewComponent(Name = "ProductAttributes")] + public class ProductAttributesViewComponent : NopViewComponent + { + private const string NET_WEIGHT_KEY = nameof(IMeasuringAttributeValues.NetWeight); + private const string GROSS_WEIGHT_KEY = nameof(IMeasuringAttributeValues.GrossWeight); + private const string IS_MEASURABLE_KEY = nameof(IMeasuringAttributeValues.IsMeasurable); + + private readonly IGenericAttributeService _genericAttributeService; + private readonly IWorkContext _workContext; + private readonly IStoreContext _storeContext; + + public ProductAttributesViewComponent( + IGenericAttributeService genericAttributeService, + IWorkContext workContext, + IStoreContext storeContext) + { + _genericAttributeService = genericAttributeService; + _workContext = workContext; + _storeContext = storeContext; + } + + public async Task InvokeAsync(string widgetZone, object additionalData) + { + + + if (additionalData is not ProductModel product) + return Content(""); + + var model = new ProductAttributesModel + { + ProductId = product.Id + }; + + //get store scope + var storeScope = await _storeContext.GetCurrentStoreAsync(); + + + if (product.Id > 0) + { + // Load existing values + var dbProduct = new Core.Domain.Catalog.Product { Id = product.Id }; + + //var dbMesaurable = await _genericAttributeService.GetAttributeAsync(dbProduct, IS_MEASURABLE_KEY, storeScope.Id); + //var dbNetWeight = await _genericAttributeService.GetAttributeAsync(dbProduct, NET_WEIGHT_KEY, storeScope.Id); + //var dbIncomingQuantity = await _genericAttributeService.GetAttributeAsync(dbProduct, "IncomingQuantity", storeScope.Id); + + model.IsMeasurable = await _genericAttributeService + .GetAttributeAsync(dbProduct, IS_MEASURABLE_KEY, storeScope.Id); + + model.NetWeight = await _genericAttributeService + .GetAttributeAsync(dbProduct, NET_WEIGHT_KEY, storeScope.Id); + + model.IncomingQuantity = await _genericAttributeService + .GetAttributeAsync(dbProduct, "IncomingQuantity", storeScope.Id); + } + + return View("~/Plugins/Misc.FruitBankPlugin/Views/ProductAttributes.cshtml", model); + } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs index aadf6c2..6c7f39b 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs @@ -1,5 +1,6 @@ using AyCode.Core.Loggers; using AyCode.Interfaces.Entities; +using DevExpress.XtraPrinting.Native; using FruitBank.Common.Entities; using FruitBank.Common.Interfaces; using FruitBank.Common.Loggers; @@ -18,12 +19,15 @@ using Nop.Services.Events; namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.EventConsumers; -public class FruitBankEventConsumer(IHttpContextAccessor httpContextAccessor, FruitBankDbContext ctx, FruitBankAttributeService fruitBankAttributeService, IStoreContext storeContext, IEnumerable logWriters) : +public class FruitBankEventConsumer(IHttpContextAccessor httpContextAccessor, FruitBankDbContext ctx, FruitBankAttributeService fruitBankAttributeService, IStoreContext storeContext, IEnumerable logWriters, IGenericAttributeService genericAttributeService) : MgEventConsumer(httpContextAccessor, logWriters), IConsumer>, IConsumer> { public override async Task HandleEventAsync(EntityUpdatedEvent eventMessage) { var product = eventMessage.Entity; + + await SaveCustomAttributesAsync(eventMessage.Entity); + var isMeasurableProduct = await fruitBankAttributeService.IsMeasurableEntityAsync(product.Id); var shippingItems = await ctx.ShippingItems.Table @@ -37,6 +41,55 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAccessor, Fr await base.HandleEventAsync(eventMessage); } + + public async Task HandleEventAsync(EntityInsertedEvent eventMessage) + { + await SaveCustomAttributesAsync(eventMessage.Entity); + } + + private async Task SaveCustomAttributesAsync(Product product) + { + if (product == null) + return; + + var form = httpContextAccessor.HttpContext?.Request?.Form; + if (form == null || !form.Any()) + return; + + var isMeasurable = form["IsMeasurable"].ToString().Contains("true"); + + // Save IsMeasurable + if (form.ContainsKey("IsMeasurable")) + { + await genericAttributeService.SaveAttributeAsync(product, "IsMeasurable", isMeasurable); + //Akkor ez kell? - Á. + //await fruitBankAttributeService.InsertOrUpdateMeasuringAttributeValuesAsync(product.Id, 0, 0, isMeasurable, false); + } + + // Save NetWeight + if (form.ContainsKey("NetWeight")) + { + var netWeightStr = form["NetWeight"].ToString(); + if (!string.IsNullOrWhiteSpace(netWeightStr) && decimal.TryParse(netWeightStr, out var netWeight)) + { + await genericAttributeService.SaveAttributeAsync(product, "NetWeight", netWeight); + + //await fruitBankAttributeService.InsertOrUpdateMeasuringAttributeValuesAsync(product.Id, 0, 0, , false); + } + } + + // Save IncomingQuantity + if (form.ContainsKey("IncomingQuantity")) + { + var incomingQtyStr = form["IncomingQuantity"].ToString(); + if (!string.IsNullOrWhiteSpace(incomingQtyStr) && int.TryParse(incomingQtyStr, out var incomingQuantity)) + { + await genericAttributeService.SaveAttributeAsync(product, "IncomingQuantity", incomingQuantity); + } + } + } + + public async Task HandleEventAsync(EntityInsertedEvent eventMessage) { Logger.Info($"HandleEventAsync EntityInsertedEvent; id: {eventMessage.Entity.Id}"); diff --git a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs index 043578a..6104a13 100644 --- a/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs +++ b/Nop.Plugin.Misc.AIPlugin/FruitBankPlugin.cs @@ -84,7 +84,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin public Task> GetWidgetZonesAsync() { - return Task.FromResult>(new List { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom }); + return Task.FromResult>(new List { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock }); } //public string GetWidgetViewComponentName(string widgetZone) @@ -128,7 +128,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin var zones = GetWidgetZonesAsync().Result; - return zones.Any(widgetZone.Equals) ? typeof(ProductAIWidgetViewComponent) : null; + if (zones.Any(widgetZone.Equals)) + { + if (widgetZone == PublicWidgetZones.ProductBoxAddinfoBefore || widgetZone == PublicWidgetZones.ProductDetailsBottom) + { + return zones.Any(widgetZone.Equals) ? typeof(ProductAIWidgetViewComponent) : null; + } + else if (widgetZone == AdminWidgetZones.ProductDetailsBlock) + { + return zones.Any(widgetZone.Equals) ? typeof(ProductAttributesViewComponent) : null; + } + } + + return null; + } } } diff --git a/Nop.Plugin.Misc.AIPlugin/Models/ProductAttributesModel.cs b/Nop.Plugin.Misc.AIPlugin/Models/ProductAttributesModel.cs new file mode 100644 index 0000000..8be1872 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Models/ProductAttributesModel.cs @@ -0,0 +1,19 @@ +using Nop.Web.Framework.Models; +using Nop.Web.Framework.Mvc.ModelBinding; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Models +{ + public record ProductAttributesModel : BaseNopModel + { + public int ProductId { get; set; } + + [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.IsMeasurable")] + public bool IsMeasurable { get; set; } + + [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.NetWeight")] + public decimal? NetWeight { get; set; } + + [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.IncomingQuantity")] + public int? IncomingQuantity { get; set; } + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj index b157f5a..41014e9 100644 --- a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj +++ b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj @@ -605,6 +605,12 @@ Always + + Always + + + Always + Always diff --git a/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml new file mode 100644 index 0000000..e8574ed --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml @@ -0,0 +1,41 @@ +@* File: Plugins/Nop.Plugin.YourCompany.ProductAttributes/Views/ProductCustomAttributes.cshtml *@ + +@model Nop.Plugin.Misc.FruitBankPlugin.Models.ProductAttributesModel + +
+
+ + Custom Product Attributes +
+
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+
+
\ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Views/ProductList.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/ProductList.cshtml new file mode 100644 index 0000000..ac914c3 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Views/ProductList.cshtml @@ -0,0 +1,54 @@ +@* File: Plugins/YourCompany.ProductAttributes/Views/ProductList.cshtml *@ +@* This view component will inject into the product list page *@ + + + + + From 13a1eb204cba2fa8e8986eff62c38b2f8a11341d Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 11 Oct 2025 00:07:35 +0200 Subject: [PATCH 3/3] fix --- .../EventConsumers/FruitBankEventConsumer.cs | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs index 5c79013..1cfcb62 100644 --- a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs +++ b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs @@ -18,7 +18,7 @@ using Nop.Services.Events; namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.EventConsumers; -public class FruitBankEventConsumer(IHttpContextAccessor httpContextAccessor, FruitBankDbContext ctx, FruitBankAttributeService fruitBankAttributeService, IStoreContext storeContext, IEnumerable logWriters) : +public class FruitBankEventConsumer(IHttpContextAccessor httpContextAccessor, FruitBankDbContext ctx, FruitBankAttributeService fruitBankAttributeService, IStoreContext storeContext, IEnumerable logWriters, IGenericAttributeService genericAttributeService) : MgEventConsumer(httpContextAccessor, logWriters), IConsumer>, IConsumer>, @@ -34,6 +34,9 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAccessor, Fr public override async Task HandleEventAsync(EntityUpdatedEvent eventMessage) { var product = eventMessage.Entity; + + await SaveCustomAttributesAsync(eventMessage.Entity); + var isMeasurableProduct = await fruitBankAttributeService.IsMeasurableEntityAsync(product.Id); var shippingItems = await ctx.ShippingItems.Table @@ -47,6 +50,53 @@ public class FruitBankEventConsumer(IHttpContextAccessor httpContextAccessor, Fr await base.HandleEventAsync(eventMessage); } + public async Task HandleEventAsync(EntityInsertedEvent eventMessage) + { + await SaveCustomAttributesAsync(eventMessage.Entity); + } + + private async Task SaveCustomAttributesAsync(Product product) + { + if (product == null) + return; + + var form = httpContextAccessor.HttpContext?.Request?.Form; + if (form == null || !form.Any()) + return; + + var isMeasurable = form["IsMeasurable"].ToString().Contains("true"); + + // Save IsMeasurable + if (form.ContainsKey("IsMeasurable")) + { + await genericAttributeService.SaveAttributeAsync(product, "IsMeasurable", isMeasurable); + //Akkor ez kell? - Á. + //await fruitBankAttributeService.InsertOrUpdateMeasuringAttributeValuesAsync(product.Id, 0, 0, isMeasurable, false); + } + + // Save NetWeight + if (form.ContainsKey("NetWeight")) + { + var netWeightStr = form["NetWeight"].ToString(); + if (!string.IsNullOrWhiteSpace(netWeightStr) && decimal.TryParse(netWeightStr, out var netWeight)) + { + await genericAttributeService.SaveAttributeAsync(product, "NetWeight", netWeight); + //Akkor ez kell? - Á. + //await fruitBankAttributeService.InsertOrUpdateMeasuringAttributeValuesAsync(product.Id, 0, 0, , false); + } + } + + // Save IncomingQuantity + if (form.ContainsKey("IncomingQuantity")) + { + var incomingQtyStr = form["IncomingQuantity"].ToString(); + if (!string.IsNullOrWhiteSpace(incomingQtyStr) && int.TryParse(incomingQtyStr, out var incomingQuantity)) + { + await genericAttributeService.SaveAttributeAsync(product, "IncomingQuantity", incomingQuantity); + } + } + } + public async Task HandleEventAsync(EntityInsertedEvent eventMessage) { Logger.Info($"HandleEventAsync EntityInsertedEvent; id: {eventMessage.Entity.Id}");