Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/ManagementPageController.cs

699 lines
31 KiB
C#

using AyCode.Core.Loggers;
using DevExpress.Pdf.Native;
using FruitBank.Common;
using FruitBank.Common.Entities;
using Mango.Nop.Core.Loggers;
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.Services;
using Nop.Services.Security;
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]
public class ManagementPageController : BaseAdminController
{
private readonly IPermissionService _permissionService;
protected readonly FruitBankDbContext _dbContext;
protected readonly AICalculationService _aiCalculationService;
protected readonly OpenAIApiService _openAIApiService;
private readonly ILogger _logger;
public ManagementPageController(IPermissionService permissionService, FruitBankDbContext fruitBankDbContext, AICalculationService aiCalculationService, OpenAIApiService openAIApiService, IEnumerable<IAcLogWriterBase> logWriters)
{
_logger = new Logger<ManagementPageController>(logWriters.ToArray());
_permissionService = permissionService;
_dbContext = fruitBankDbContext;
_aiCalculationService = aiCalculationService;
_openAIApiService = openAIApiService;
}
public async Task<IActionResult> Test()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
var testPageModel = new TestPageModel();
testPageModel.Grids = new List<TestGridModel>();
var testGridModel2 = new TestGridModel();
testGridModel2.GridName = "Orders";
testGridModel2.ViewComponentName = "ShippingDocumentGridComponent";
testGridModel2.ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml";
testGridModel2.Configuration = new GridConfiguration();
testGridModel2.Configuration.ShowChildGridsAsTabs = true;
testGridModel2.ChildGrids = new List<TestGridModel>();
var childGrid1 = new TestGridModel
{
GridName = "TestGrid",
ViewComponentName = "TestGridComponent",
ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml",
ParentGridId = testGridModel2.Id,
ChildGrids = new List<TestGridModel>()
};
testGridModel2.ChildGrids.Add(childGrid1);
var childGrid2 = new TestGridModel
{
GridName = "Files",
ViewComponentName = "FileUploadGridComponent",
ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml",
ParentGridId = testGridModel2.Id,
ChildGrids = new List<TestGridModel>()
};
testGridModel2.ChildGrids.Add(childGrid2);
testPageModel.Grids.Add(testGridModel2);
var testGridModel = new TestGridModel();
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);
}
[HttpPost]
public IActionResult LoadChildGrid([FromBody] LoadChildGridRequest request)
{
// request.contextId is the actual row ID (data.Id from DevExtreme)
// request.childModel is the full TestGridModel object
// Add the context ID to the model's DataContext
if (request.ChildModel.DataContext == null)
request.ChildModel.DataContext = new Dictionary<string, int>();
request.ChildModel.DataContext["contextId"] = request.ContextId;
// Invoke the view component with the full model
return ViewComponent(request.ChildModel.ViewComponentName, request.ChildModel);
}
// Request model for deserialization
public class LoadChildGridRequest
{
public int ContextId { get; set; } // The actual row ID from data.Id
public TestGridModel ChildModel { get; set; } // The full child grid model
}
[HttpGet]
public async Task<IActionResult> GetShippings()
{
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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);
}
[HttpGet]
public async Task<IActionResult> GetAllPartners()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
// Mock data for now
var model = await _dbContext.Partners.GetAll().ToListAsync();
var valami = model;
//model. = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId);
return Json(model);
}
[HttpPost]
[RequestSizeLimit(10485760)] // 10MB
[RequestFormLimits(MultipartBodyLengthLimit = 10485760)]
public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId, int? partnerId)
{
var shippingDocument = await _dbContext.ShippingDocuments.GetByIdAsync(shippingDocumentId);
//checks
// - files exist
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
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)
{
_logger.Debug($"Associated with Partner ID: {partnerId.Value}");
//let's get the partner
var partner = await _dbContext.Partners.GetByIdAsync(partnerId.Value);
}
var filesList = new List<Files>();
//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 = "";
_logger.Detail($"Received file: {fileName} for Document ID: {shippingDocumentId}, content type: {file.ContentType}");
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) && !file.ContentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase))
return Json(new { success = false, errorMessage = "Only PDF or jpg files are allowed" });
// Validate file size (max 20MB)
if (file.Length > 20 * 1024 * 1024)
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")
if (file.Length > 0)
{
if (file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)){
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);
}
//still nothing? let's send it to AI
if (string.IsNullOrWhiteSpace(pdfText))
{
try
{
// ✅ Use the service we implemented earlier
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this PDF.");
}
catch (Exception aiEx)
{
Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
}
}
// For demonstration, let's just log the extracted text
_logger.Detail($"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}");
}
}
else //read from image
{
try
{
// Open the Image from the IFormFile's stream directly in memory
using (var stream = file.OpenReadStream())
{
try
{
// ✅ Use the service we implemented earlier
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this image.");
}
catch (Exception aiEx)
{
Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
}
// For demonstration, let's just log the extracted text
_logger.Detail($"Extracted text from {file.FileName}: {pdfText}");
}
}
catch (Exception ex)
{
// Handle potential exceptions during PDF processing
Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}");
return StatusCode(500, $"Error processing PDF file: {ex.Message}");
}
}
//we should have some kind of text now
_logger.Detail(pdfText);
}
string analysisPrompt = "Extract the document identification number from this document, determine the type of the " +
"document IN ENGLISH from the available list, and return them as JSON: documentNumber, documentType. " +
$"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;
}
var transactionSuccess = await _dbContext.TransactionSafeAsync(async _ =>
{
await _dbContext.Files.InsertAsync(dbFile);
filesList.Add(dbFile);
var shippingDocumentToFiles = new ShippingDocumentToFiles
{
ShippingDocumentId = shippingDocumentId,
FilesId = dbFile.Id,
DocumentType = extractedMetaData.DocumentType != null ? (DocumentType)Enum.Parse(typeof(DocumentType), extractedMetaData.DocumentType) : DocumentType.Unknown
};
_logger.Detail(shippingDocumentToFiles.DocumentType.ToString());
await _dbContext.ShippingDocumentToFiles.InsertAsync(shippingDocumentToFiles);
return true;
});
if (!transactionSuccess) _logger.Error($"(transactionSuccess == false)");
// - 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(), partnerAnalysisPrompt);
var extractedPartnerData = ParsePartnerDataAIResponse(partnerAnalyzis);
if (extractedPartnerData.Name != null)
{
_logger.Detail("AI Analysis Partner Result:");
_logger.Detail(extractedPartnerData.Name);
}
if (extractedPartnerData.TaxId != null)
{
_logger.Detail(extractedPartnerData.TaxId);
}
if (extractedPartnerData.Country != null)
{
_logger.Detail(extractedPartnerData.Country);
}
if (extractedPartnerData.State != null)
{
_logger.Detail(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", "orders", "order" + shippingDocumentId.ToString(), "documents");
Directory.CreateDirectory(uploadsPath);
// Generate unique filename
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);
}
}
catch (Exception ex)
{
_logger.Error($"Error saving file: {ex.Message}", ex);
//return Json(new { success = false, errorMessage = ex.Message });
return BadRequest("No files were uploaded.");
}
// - 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.GetOpenAIPDFAnalysisFromText(
pdfText.ToString(),
"You work for FruitBank. 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
_logger.Detail("AI Analysis Result:");
_logger.Detail(extractedData.RecipientName);
_logger.Detail(extractedData.SenderName);
_logger.Detail(extractedData.InvoiceNumber);
_logger.Detail(extractedData.TotalAmount.ToString());
_logger.Detail(extractedData.ItemCount.ToString());
_logger.Detail(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
//});
}
catch (Exception ex)
{
_logger.Error($"Error uploading file: {ex.Message}", 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<IActionResult> UploadFile(List<IFormFile> 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.
// _logger.Detail($"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
{
// Try to parse as JSON first
var data = System.Text.Json.JsonSerializer.Deserialize<ExtractedDocumentData>(
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}"
};
}
}
private ExtractedDocumentMetaData ParseMetaDataAIResponse(string aiResponse)
{
try
{
// Try to parse as JSON first
var data = System.Text.Json.JsonSerializer.Deserialize<ExtractedDocumentMetaData>(
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<ExtractedPartnerData>(
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
{
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<IActionResult> 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<IFormFile> Files { get; set; }
}
}