diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/PartnersGridComponent.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/PartnersGridComponent.cs new file mode 100644 index 0000000..7dc7060 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/PartnersGridComponent.cs @@ -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 = "PartnersGridComponent")] + public class PartnersGridComponent : ViewComponent + { + public async Task InvokeAsync(TestGridModel model) + { + // Here you can fetch data for this grid if needed + // For demo, just pass the model + return View(model.ViewComponentName +".cshtml", model); + } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/ShippingDocumentGridComponent.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/ShippingDocumentGridComponent.cs new file mode 100644 index 0000000..e1ee88a --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/ShippingDocumentGridComponent.cs @@ -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 = "ShippingDocumentGridComponent")] + public class ShippingDocumentGridComponent : ViewComponent + { + public async Task InvokeAsync(TestGridModel model) + { + // Here you can fetch data for this grid if needed + // For demo, just pass the model + return View(model.ViewComponentName +".cshtml", model); + } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/ShippingGridComponent.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/ShippingGridComponent.cs new file mode 100644 index 0000000..037f2ad --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/ShippingGridComponent.cs @@ -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 = "ShippingGridComponent")] + public class ShippingGridComponent : ViewComponent + { + public async Task InvokeAsync(TestGridModel model) + { + // Here you can fetch data for this grid if needed + // For demo, just pass the model + return View(model.ViewComponentName +".cshtml", model); + } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml new file mode 100644 index 0000000..400c358 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml @@ -0,0 +1,25 @@ +@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 +

Id: @Model.ShippingId

+ +@(Html.DevExtreme().DataGrid() + .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); + }) +) \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FruitBankPluginAdminController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FruitBankPluginAdminController.cs index a970e13..8853f57 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FruitBankPluginAdminController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FruitBankPluginAdminController.cs @@ -29,6 +29,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers { ApiKey = _settings.ApiKey, ModelName = _settings.ModelName, + OpenAIApiKey = _settings.OpenAIApiKey, + OpenAIModelName = _settings.OpenAIModelName, IsEnabled = _settings.IsEnabled, ApiBaseUrl = _settings.ApiBaseUrl, MaxTokens = _settings.MaxTokens, @@ -49,8 +51,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers // Map model properties to settings _settings.ApiKey = model.ApiKey ?? string.Empty; _settings.ModelName = model.ModelName ?? string.Empty; + _settings.OpenAIApiKey = model.OpenAIApiKey ?? string.Empty; + _settings.OpenAIModelName = model.OpenAIModelName ?? string.Empty; _settings.IsEnabled = model.IsEnabled; _settings.ApiBaseUrl = model.ApiBaseUrl ?? string.Empty; + _settings.OpenAIApiBaseUrl = model.OpenAIApiBaseUrl ?? string.Empty; _settings.MaxTokens = model.MaxTokens; _settings.Temperature = model.Temperature; _settings.RequestTimeoutSeconds = model.RequestTimeoutSeconds; diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs new file mode 100644 index 0000000..ce0c8f3 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs @@ -0,0 +1,322 @@ +using FruitBank.Common.Entities; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Azure; +using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models; +using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; +using Nop.Plugin.Misc.FruitBankPlugin.Services; +using Nop.Services.Security; +using Nop.Web.Areas.Admin.Controllers; +using Nop.Web.Framework; +using Nop.Web.Framework.Mvc.Filters; +using System.Text; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers +{ + [Area(AreaNames.ADMIN)] + [AuthorizeAdmin] + public class ManagementPageController : BaseAdminController + { + private readonly IPermissionService _permissionService; + protected readonly FruitBankDbContext _dbContext; + protected readonly AICalculationService _aiCalculationService; + + public ManagementPageController(IPermissionService permissionService, FruitBankDbContext fruitBankDbContext, AICalculationService aiCalculationService) + { + _permissionService = permissionService; + _dbContext = fruitBankDbContext; + _aiCalculationService = aiCalculationService; + } + + public async Task Test() + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return AccessDeniedView(); + var testPageModel = new TestPageModel(); + testPageModel.Grids = new List(); + + var testGridModel2 = new TestGridModel(); + testGridModel2.GridName = "Orders"; + testGridModel2.ViewComponentName = "ShippingDocumentGridComponent"; + testPageModel.Grids.Add(testGridModel2); + + var testGridModel = new TestGridModel(); + testGridModel.GridName = "Shipping"; + testGridModel.ViewComponentName = "ShippingGridComponent"; + testPageModel.Grids.Add(testGridModel); + + var testGridModel3 = new TestGridModel(); + testGridModel3.GridName = "Partners"; + testGridModel3.ViewComponentName = "PartnersGridComponent"; + testPageModel.Grids.Add(testGridModel3); + + //testGridModel.ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Components/Views/_ShippingGridComponent.cshtml"; + return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/Test.cshtml", testPageModel); + } + + [HttpGet] + public async Task GetShippings() + { + 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; + //model. = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId); + return Json(model); + } + + [HttpGet] + public async Task GetShippingDocuments() + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return AccessDeniedView(); + + // Mock data for now + var model = await _dbContext.ShippingDocuments.GetAll(true).OrderByDescending(sd => sd.Created).ToListAsync(); + var valami = model; + //model. = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId); + return Json(model); + } + + [HttpPost] + public async Task ShippingDocumentList(int shippingId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return AccessDeniedView(); + + // Apply filters to mock data + var model = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId); + + return Json(model); + } + + [HttpGet] + public async Task GetPartners() + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return AccessDeniedView(); + + // Mock data for now + var model = await _dbContext.Partners.GetAll().OrderByDescending(p => p.Created).ToListAsync(); + var valami = model; + //model. = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId); + return Json(model); + } + + [HttpGet] + public async Task GetShippingItemsByShippingDocumentId(int shippingDocumentId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return AccessDeniedView(); + + // Mock data for now + var model = await _dbContext.ShippingItems.GetAll(true).Where(sd => sd.ShippingDocumentId == shippingDocumentId).OrderByDescending(sd => sd.Created).ToListAsync(); + var valami = model; + //model. = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId); + return Json(model); + } + + [HttpGet] + public async Task GetShippingDocumentsByShippingDocumentId(int shippingDocumentId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return AccessDeniedView(); + + // Mock data for now + var model = await _dbContext.ShippingDocumentToFiles.GetAll().Where(f => f.ShippingDocumentId == shippingDocumentId).OrderByDescending(sd => sd.Created).ToListAsync(); + var valami = model; + //model. = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId); + return Json(model); + } + + [HttpPost] + [RequestSizeLimit(10485760)] // 10MB + [RequestFormLimits(MultipartBodyLengthLimit = 10485760)] + public async Task UploadFile(UploadModel model) + { + var files = model.Files; + var shippingDocumentId = model.ShippingDocumentId; + try + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, errorMessage = "Access denied" }); + + if (files == null || files.Count == 0) + return Json(new { success = false, errorMessage = "No file selected" }); + + foreach (var file in files) + { + // 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" }); + + // Create upload directory if it doesn't exist + var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shippingDocuments"); + Directory.CreateDirectory(uploadsPath); + + // Generate unique filename + var fileName = $"{Guid.NewGuid()}_{file.FileName}"; + var filePath = Path.Combine(uploadsPath, fileName); + + // Save file + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + // 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()); + + // Analyze PDF with AI to extract structured data + var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalyzisFromText( + 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." + ); + + // Parse AI response (assuming it returns JSON) + var extractedData = ParseShippingDocumentAIResponse(aiAnalysis); + + //TODO: Save document record to database + Console.WriteLine("AI Analysis Result:"); + Console.WriteLine(extractedData.RecipientName); + Console.WriteLine(extractedData.SenderName); + Console.WriteLine(extractedData.InvoiceNumber); + Console.WriteLine(extractedData.TotalAmount); + Console.WriteLine(extractedData.ItemCount); + Console.WriteLine(extractedData.Notes); + + + var documentId = 1; // Replace with: savedDocument.Id + + // Return structured document model + var documentModel = new ShippingDocumentModel + { + Id = documentId, + ShippingId = shippingDocumentId, + FileName = file.FileName, + FilePath = $"/uploads/shippingDocuments/{fileName}", + FileSize = (int)(file.Length / 1024), + DocumentDate = extractedData.DocumentDate, + RecipientName = extractedData.RecipientName, + SenderName = extractedData.SenderName, + InvoiceNumber = extractedData.InvoiceNumber, + TotalAmount = extractedData.TotalAmount, + ItemCount = extractedData.ItemCount, + Notes = extractedData.Notes, + 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 + //}); + + 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."); + } + } + + private ExtractedDocumentData ParseShippingDocumentAIResponse(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 ExtractedDocumentData(); + } + catch + { + // If JSON parsing fails, return empty data with the raw response in notes + return new ExtractedDocumentData + { + Notes = $"AI Analysis (raw): {aiResponse}" + }; + } + } + + // Helper class for extracted data + private class ExtractedDocumentData + { + public DateTime? DocumentDate { get; set; } + public string RecipientName { get; set; } + public string SenderName { get; set; } + public string InvoiceNumber { get; set; } + public decimal? TotalAmount { get; set; } + public int? ItemCount { get; set; } + public string Notes { get; set; } + } + + [HttpPost] + public async Task DeleteUploadedFile(string filePath) + { + try + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + if (string.IsNullOrEmpty(filePath)) + return Json(new { success = false, message = "Invalid file path" }); + + // TODO: Delete document record from database first + // var document = await _documentService.GetDocumentByFilePathAsync(filePath); + // if (document != null) + // await _documentService.DeleteDocumentAsync(document); + + var fullPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", filePath.TrimStart('/')); + + if (System.IO.File.Exists(fullPath)) + { + System.IO.File.Delete(fullPath); + return Json(new { success = true }); + } + + return Json(new { success = false, message = "File not found" }); + } + catch (Exception ex) + { + return Json(new { success = false, message = ex.Message }); + } + } + + } + + public class UploadModel + { + public int ShippingDocumentId { get; set; } + public List Files { get; set; } + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ShippingController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ShippingController.cs index e464330..2215faf 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ShippingController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ShippingController.cs @@ -1,8 +1,12 @@ -using FruitBank.Common.Entities; +using DevExtreme.AspNet.Data; +using DevExtreme.AspNet.Mvc; +using FruitBank.Common.Entities; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; +using Nop.Plugin.Misc.FruitBankPlugin.Helpers; +using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Messages; using Nop.Services.Security; using Nop.Web.Areas.Admin.Controllers; @@ -11,6 +15,7 @@ using Nop.Web.Framework; using Nop.Web.Framework.Models; using Nop.Web.Framework.Models.Extensions; using Nop.Web.Framework.Mvc.Filters; +using System.Text; namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers { @@ -24,12 +29,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers protected readonly ShippingDbTable _shippingDbTable; protected readonly ShippingDocumentDbTable _shippingDocumentDbTable; protected readonly FruitBankDbContext _dbContext; - //private readonly IFruitBankShippingModelFactory _shippingModelFactory; - // TODO: Add your shipment and document services here - // private readonly IShipmentService _shipmentService; - // private readonly IShipmentDocumentService _documentService; + protected readonly AICalculationService _aiCalculationService; - public ShippingController(IPermissionService permissionService, INotificationService notificationService, ShippingItemDbTable shippingItemDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, FruitBankDbContext dbContext) + + public ShippingController(IPermissionService permissionService, INotificationService notificationService, ShippingItemDbTable shippingItemDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, FruitBankDbContext dbContext, AICalculationService aICalculationService) { _permissionService = permissionService; _notificationService = notificationService; @@ -38,6 +41,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers _shippingDocumentDbTable = shippingDocumentDbTable; _dbContext = dbContext; //_shippingModelFactory = shippingModelFactory; + _aiCalculationService = aICalculationService; } [HttpGet] @@ -79,13 +83,13 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return AccessDeniedView(); // Create model and load data - var model = _shippingDocumentDbTable.GetAll().ToList(); - + //var model = _shippingDocumentDbTable.GetAll().ToList(); + var model = _dbContext.ShippingDocuments.GetAll(true).ToList(); // TODO: Replace with your actual service call // model.ShippingList = await _shippingService.GetAllShippingsAsync(); // Mock data for now - + return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/List.cshtml", model); } @@ -102,6 +106,36 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/List.cshtml", model); } + + //TODO IMPLEMENT DEVEXTREME FILETRING AND LOADING + //[HttpGet] + //public async Task GetDocumentByShippingId(int shippingId, DataSourceLoadOptions loadOptions) + //{ + // // 1. Get the IQueryable from your service. + // // This is the key step. It's more efficient to work with IQueryable + // // so that filtering, sorting, and paging are done by the database. + // var documentQuery = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId); + + // // 2. Process the data using DevExtreme.AspNet.Data. + // // This is where all the magic happens. The Load method takes care of + // // applying all the grid's client-side settings (sorting, filtering, + // // grouping, paging) to your data query. + // var loadResult = DataSourceLoader.Load(documentQuery, loadOptions); + + // // 3. Return the result in the format DevExtreme expects. + // return Json(loadResult); + //} + + [HttpGet] + public async Task GetShippingDocumentsByShippingId(int shippingId) + { + + var result = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId); + + // 3. Return the result in the format DevExtreme expects. + return Json(result); + } + [HttpPost] public async Task Delete(int id) { @@ -246,26 +280,28 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers } [HttpPost] - public async Task UploadFile(IFormFile file, int shipmentId) + [RequestSizeLimit(10485760)] // 10MB + [RequestFormLimits(MultipartBodyLengthLimit = 10485760)] + public async Task UploadFile(IFormFile file, int shippingId) { try { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) - return Json(new FileUploadResult { Success = false, ErrorMessage = "Access denied" }); + return Json(new { success = false, errorMessage = "Access denied" }); if (file == null || file.Length == 0) - return Json(new FileUploadResult { Success = false, ErrorMessage = "No file selected" }); + return Json(new { success = false, errorMessage = "No file selected" }); // Validate file type (PDF only) if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) - return Json(new FileUploadResult { Success = false, ErrorMessage = "Only PDF files are allowed" }); + return Json(new { success = false, errorMessage = "Only PDF files are allowed" }); - // Validate file size (e.g., max 10MB) + // Validate file size (max 10MB) if (file.Length > 10 * 1024 * 1024) - return Json(new FileUploadResult { Success = false, ErrorMessage = "File size must be less than 10MB" }); + return Json(new { success = false, errorMessage = "File size must be less than 10MB" }); // Create upload directory if it doesn't exist - var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shipments"); + var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shippingDocuments"); Directory.CreateDirectory(uploadsPath); // Generate unique filename @@ -278,32 +314,106 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers await file.CopyToAsync(stream); } - // TODO: Save document record to database - // var document = new ShipmentDocument - // { - // ShipmentId = shipmentId, - // FileName = file.FileName, - // FilePath = $"/uploads/shipments/{fileName}", - // FileSize = (int)(file.Length / 1024), // Convert to KB - // ContentType = file.ContentType, - // UploadDate = DateTime.UtcNow, - // IsActive = true - // }; + // 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()); + + // Analyze PDF with AI to extract structured data + var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalyzisFromText( + 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." + ); + + // Parse AI response (assuming it returns JSON) + var extractedData = ParseShippingDocumentAIResponse(aiAnalysis); + + //TODO: Save document record to database + Console.WriteLine("AI Analysis Result:"); + Console.WriteLine(extractedData.RecipientName); + Console.WriteLine(extractedData.SenderName); + Console.WriteLine(extractedData.InvoiceNumber); + Console.WriteLine(extractedData.TotalAmount); + Console.WriteLine(extractedData.ItemCount); + Console.WriteLine(extractedData.Notes); + // var savedDocument = await _documentService.InsertDocumentAsync(document); - return Json(new FileUploadResult + // Mock saved document ID + var documentId = 1; // Replace with: savedDocument.Id + + // Return structured document model + var documentModel = new ShippingDocumentModel { - Success = true, + Id = documentId, + ShippingId = shippingId, FileName = file.FileName, FilePath = $"/uploads/shippingDocuments/{fileName}", - FileSize = (int)(file.Length / 1024), // KB - DocumentId = 1 // Replace with: savedDocument.Id + FileSize = (int)(file.Length / 1024), + DocumentDate = extractedData.DocumentDate, + RecipientName = extractedData.RecipientName, + SenderName = extractedData.SenderName, + InvoiceNumber = extractedData.InvoiceNumber, + TotalAmount = extractedData.TotalAmount, + ItemCount = extractedData.ItemCount, + Notes = extractedData.Notes, + RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging + }; + + return Json(new + { + success = true, + document = documentModel }); } catch (Exception ex) { - return Json(new FileUploadResult { Success = false, ErrorMessage = ex.Message }); + Console.WriteLine($"Error uploading file: {ex}"); + return Json(new { success = false, errorMessage = ex.Message }); } + } + + private ExtractedDocumentData ParseShippingDocumentAIResponse(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 ExtractedDocumentData(); + } + catch + { + // If JSON parsing fails, return empty data with the raw response in notes + return new ExtractedDocumentData + { + Notes = $"AI Analysis (raw): {aiResponse}" + }; + } + } + + // Helper class for extracted data + private class ExtractedDocumentData + { + public DateTime? DocumentDate { get; set; } + public string RecipientName { get; set; } + public string SenderName { get; set; } + public string InvoiceNumber { get; set; } + public decimal? TotalAmount { get; set; } + public int? ItemCount { get; set; } + public string Notes { get; set; } } [HttpPost] diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ConfigureModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ConfigureModel.cs index 9197c6b..0b5c786 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ConfigureModel.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ConfigureModel.cs @@ -15,12 +15,21 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ModelName")] public string ModelName { get; set; } = string.Empty; + [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiKey")] + public string OpenAIApiKey { get; set; } = string.Empty; + + [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ModelName")] + public string OpenAIModelName { get; set; } = string.Empty; + [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.IsEnabled")] public bool IsEnabled { get; set; } [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiBaseUrl")] public string ApiBaseUrl { get; set; } = string.Empty; + [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiBaseUrl")] + public string OpenAIApiBaseUrl { get; set; } = string.Empty; + [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.MaxTokens")] public int MaxTokens { get; set; } diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/EditShippingModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/EditShippingModel.cs index 53508f7..124c0ee 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/EditShippingModel.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/EditShippingModel.cs @@ -27,17 +27,23 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models public class ShippingDocumentModel { public int Id { get; set; } - public ShippingDocument ShippingDocument { get; set; } + public int ShippingId { get; set; } public string FileName { get; set; } public string FilePath { get; set; } - public int FileSize { get; set; } // in KB - public DateTime UploadDate { get; set; } - public string ContentType { get; set; } - public bool IsActive { get; set; } = true; + public int FileSize { get; set; } + + // Extracted data from PDF + public DateTime? DocumentDate { get; set; } + public string RecipientName { get; set; } + public string SenderName { get; set; } + public string InvoiceNumber { get; set; } + public decimal? TotalAmount { get; set; } + public int? ItemCount { get; set; } + public string Notes { get; set; } + public string RawAIAnalysis { get; set; } + + public ShippingDocument? ShippingDocument { get; set; } - // Computed properties for display - public string FormattedFileSize => $"{FileSize:N0} KB"; - public string FormattedUploadDate => UploadDate.ToString("yyyy-MM-dd HH:mm"); } // Result model for AJAX operations diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/GridBaseViewModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/GridBaseViewModel.cs new file mode 100644 index 0000000..4d035e3 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/GridBaseViewModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models +{ + public class GridBaseViewModel + { + public int? ContextId { get; set; } + } +} diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ShippingDocumentListModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ShippingDocumentListModel.cs new file mode 100644 index 0000000..57aa90c --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ShippingDocumentListModel.cs @@ -0,0 +1,18 @@ +using FruitBank.Common.Entities; +using Nop.Web.Framework.Models; +using Nop.Web.Framework.Mvc.ModelBinding; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models +{ + public record ShippingDocumentListModel : BaseNopModel + { + public ShippingDocumentListModel() + { + ShippingDocumentList = new List(); + } + + public int ShippingId { get; set; } + public List ShippingDocumentList { get; set; } + + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ShippingGridPartialViewModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ShippingGridPartialViewModel.cs new file mode 100644 index 0000000..b3de40a --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/ShippingGridPartialViewModel.cs @@ -0,0 +1,11 @@ +//using Nop.Web.Framework.Models; +//using Nop.Web.Framework.Mvc.ModelBinding; +//using System.ComponentModel.DataAnnotations; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models +{ + public class ShippingGridPartialViewModel : GridBaseViewModel + { + + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestGridModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestGridModel.cs new file mode 100644 index 0000000..29253f2 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestGridModel.cs @@ -0,0 +1,18 @@ +//using Nop.Web.Framework.Models; +//using Nop.Web.Framework.Mvc.ModelBinding; +//using System.ComponentModel.DataAnnotations; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models +{ + public class TestGridModel + { + public Guid Id = Guid.NewGuid(); + public string GridName { get; set; } + //public string GridControllerName { get; set; } + //public string GridEndpointName { get; set; } + public string ViewComponentName { get; set; } + public string ViewComponentLocation { get; set; } + + public int? ParentRowId { get; set; } + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestPageModel.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestPageModel.cs new file mode 100644 index 0000000..a1a343b --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Models/TestPageModel.cs @@ -0,0 +1,11 @@ +//using Nop.Web.Framework.Models; +//using Nop.Web.Framework.Mvc.ModelBinding; +//using System.ComponentModel.DataAnnotations; + +namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models +{ + public class TestPageModel + { + public List Grids { get; set; } + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Configure/Configure.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Configure/Configure.cshtml index c0549df..ddcca9b 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Configure/Configure.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Configure/Configure.cshtml @@ -31,6 +31,19 @@ Az AI modell neve (pl. gpt-3.5-turbo, gpt-4, claude-3-sonnet) + +
+ + + +
+ +
+ + + + Az AI modell neve (pl. gpt-3.5-turbo, gpt-4) +
@@ -39,6 +52,13 @@ Az API alapcíme (OpenAI, Azure OpenAI, stb.)
+
+ + + + Az API alapcíme (OpenAI, Azure OpenAI, stb.) +
+
diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/PartnersGridComponent.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/PartnersGridComponent.cshtml new file mode 100644 index 0000000..42472c3 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/PartnersGridComponent.cshtml @@ -0,0 +1,116 @@ +@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel +@using DevExtreme.AspNet.Mvc + +@{ + var contextId = Model; + // var gridId = $"dataGrid_{Guid.NewGuid():N}"; +} + +
+ @(Html.DevExtreme().DataGrid() + .ID(Model.Id.ToString()) + .ShowBorders(true) + .DataSource(ds => ds.Mvc() + .Controller("ManagementPage") + .LoadAction("GetPartners")) + .KeyExpr("Id") + .SearchPanel(sp => sp.Visible(true)) + .HeaderFilter(hf => hf.Visible(true)) + .Paging(p => p.PageSize(15)) + .Pager(p => p.Visible(true)) + .OnRowExpanded("onRowExpanded") + .Editing(editing => { + editing.Mode(GridEditMode.Cell); + editing.AllowUpdating(true); + editing.AllowAdding(true); + editing.AllowDeleting(true); + }) + .Columns(c => { + c.Add().DataField("Id").AllowEditing(false); + c.Add().DataField("Name"); + c.Add().DataField("TaxId"); + c.Add().DataField("Country"); + }) + .Toolbar(toolbar => { + toolbar.Items(items => { + items.Add() + .Name("addRowButton") + .ShowText(ToolbarItemShowTextMode.Always); + + items.Add() + .Location(ToolbarItemLocation.After) + .Widget(w => + w.Button() + .Text("Delete Selected Records") + .Icon("trash") + .Disabled(true) + .OnClick("onDeleteBtnClick") + ); + }); + }) + .MasterDetail(md => { + md.Enabled(true); + md.Template(@ +
<%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:
+ +
+ @(Html.DevExtreme().DataGrid() + .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") + ) + ) +
); + }) +) +
+ + + + \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml new file mode 100644 index 0000000..d9b544d --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml @@ -0,0 +1,300 @@ +@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel +@using DevExtreme.AspNet.Mvc + +@{ + var contextId = Model; + // var gridId = $"dataGrid_{Guid.NewGuid():N}"; +} + +
+ @( + Html.DevExtreme().DataGrid() + .ID("orderDataGridContainer") + .ShowBorders(true) + .DataSource(ds => ds.Mvc() + .Controller("ManagementPage") + .LoadAction("GetShippingDocuments")) + .KeyExpr("Id") + .SearchPanel(sp => sp.Visible(true)) + .HeaderFilter(hf => hf.Visible(true)) + .Paging(p => p.PageSize(15)) + .Pager(p => p.Visible(true)) + .OnRowExpanded("onRowExpanded") + .Editing(editing => { + editing.Mode(GridEditMode.Cell); + editing.AllowUpdating(true); + editing.AllowAdding(true); + editing.AllowDeleting(true); + }) + .Columns(c => { + + c.Add().DataField("Id").AllowEditing(false); + c.Add().DataField("Partner.Name").AllowEditing(false); + c.Add() + .Caption("Items in order") + .DataType(GridColumnDataType.Number) + .CalculateCellValue("calculateItemsCount").AllowEditing(false); + c.Add().DataField("PartnerId"); + c.Add().DataField("DocumentIdNumber"); + c.Add().DataField("IsAllMeasured"); + + c.Add() + .Caption("Completed") + .DataType(GridColumnDataType.Boolean) + .CalculateCellValue("calculateCellValue").AllowEditing(false); + }) + .Toolbar(toolbar => { + toolbar.Items(items => { + items.Add() + .Name("addRowButton") + .ShowText(ToolbarItemShowTextMode.Always); + + items.Add() + .Location(ToolbarItemLocation.After) + .Widget(w => + w.Button() + .Text("Delete Selected Records") + .Icon("trash") + .Disabled(true) + .OnClick("onDeleteBtnClick") + ); + }); + }) + .MasterDetail(md => { + md.Enabled(true); + md.Template(@ +
<%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:
+
+
+ +
+ +
+
+ + +
+
+ +
+
+
+ +
); + }) +) +
+ + + + + + + + + + \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingGridComponent.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingGridComponent.cshtml new file mode 100644 index 0000000..11bfc16 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/ShippingGridComponent.cshtml @@ -0,0 +1,116 @@ +@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel +@using DevExtreme.AspNet.Mvc + +@{ + var contextId = Model; + // var gridId = $"dataGrid_{Guid.NewGuid():N}"; +} + +
+ @(Html.DevExtreme().DataGrid() + .ID(Model.Id.ToString()) + .ShowBorders(true) + .DataSource(ds => ds.Mvc() + .Controller("ManagementPage") + .LoadAction("GetShippings")) + .KeyExpr("Id") + .SearchPanel(sp => sp.Visible(true)) + .HeaderFilter(hf => hf.Visible(true)) + .Paging(p => p.PageSize(15)) + .Pager(p => p.Visible(true)) + .OnRowExpanded("onRowExpanded") + .Editing(editing => { + editing.Mode(GridEditMode.Cell); + editing.AllowUpdating(true); + editing.AllowAdding(true); + editing.AllowDeleting(true); + }) + .Columns(c => { + c.Add().DataField("Id").AllowEditing(false); + c.Add().DataField("ShippingDate"); + c.Add().DataField("LicencePlate"); + c.Add().DataField("IsAllMeasured"); + }) + .Toolbar(toolbar => { + toolbar.Items(items => { + items.Add() + .Name("addRowButton") + .ShowText(ToolbarItemShowTextMode.Always); + + items.Add() + .Location(ToolbarItemLocation.After) + .Widget(w => + w.Button() + .Text("Delete Selected Records") + .Icon("trash") + .Disabled(true) + .OnClick("onDeleteBtnClick") + ); + }); + }) + .MasterDetail(md => { + md.Enabled(true); + md.Template(@ +
<%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:
+ +
+ @(Html.DevExtreme().DataGrid() + .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") + ) + ) +
); + }) +) +
+ + + + \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/Test.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/Test.cshtml index 48930f0..62e2fa0 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/Test.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/Test.cshtml @@ -1,4 +1,43 @@ -@{ - Layout = "_AdminLayout"; +@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestPageModel + +@{ + ViewData["Title"] = "Simple Tabs Example"; } -

TEST VIEW FROM PLUGIN

\ No newline at end of file + +

@ViewData["Title"]

+
+
+ + +
+ @for (int i = 0; i < Model.Grids.Count; i++) + { + var grid = Model.Grids[i]; +
+ @await Component.InvokeAsync(grid.ViewComponentName, new { model = grid }) +
+ } +
+ +
+
+ + + + diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Shipping/Edit.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Shipping/Edit.cshtml index 76553d4..a41b221 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Shipping/Edit.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Shipping/Edit.cshtml @@ -55,7 +55,7 @@
- +
@@ -91,32 +91,12 @@ - @if (Model.ExistingDocuments != null && Model.ExistingDocuments.Any()) - { -
-
Existing Documents:
-
- @foreach (var doc in Model.ExistingDocuments) - { -
-
- - @doc.FileName - (@doc.FileSize KB) -
-
- - -
-
- } -
+
+
Existing Documents:
+
+ @await Html.PartialAsync("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml", Model.ExistingDocuments)
- } +