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); } } }