diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs index 91aaba9..23b130d 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderController.cs @@ -7,6 +7,7 @@ using DocumentFormat.OpenXml.Spreadsheet; using FluentMigrator.Runner.Generators.Base; using FruitBank.Common.Dtos; using FruitBank.Common.Entities; +using FruitBank.Common.Enums; using FruitBank.Common.Interfaces; using FruitBank.Common.Server.Interfaces; using FruitBank.Common.Server.Services.SignalRs; @@ -324,7 +325,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers searchModel.SortColumn = columnName; - if(int.Parse(sortColumnIndex) > 0) searchModel.SortColumnDirection = sortDirection; // "asc" or "desc" + if (int.Parse(sortColumnIndex) > 0) searchModel.SortColumnDirection = sortDirection; // "asc" or "desc" else searchModel.SortColumnDirection = "desc"; } //else @@ -458,7 +459,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers //MeasurementService.OrderItemMeasuringReset //Todo: ezt orderitiemnként kéne kirakni?? - Á. var valami = await _measurementService.OrderItemMeasuringReset(model.OrderItemId); - + return RedirectToAction("Edit", "Order", new { id = model.OrderId }); } @@ -587,7 +588,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers throw new Exception($"{errorText}"); } - if(orderProductItem.Price != product.Price) + if (orderProductItem.Price != product.Price) { //manual price change unitPricesIncludeDiscounts = false; @@ -598,7 +599,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers } - var orderItem = await CreateOrderItem(product, order, orderProductItem, isMeasurable, unitPricesIncludeDiscounts, customer, store); + var orderItem = await CreateOrderItem(product, order, orderProductItem, isMeasurable, unitPricesIncludeDiscounts, customer, store); await _orderService.InsertOrderItemAsync(orderItem); await _productService.AdjustInventoryAsync(product, -orderItem.Quantity, orderItem.AttributesXml, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.PlaceOrder"), order.Id)); @@ -647,10 +648,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers } - // Calculate tax - //var (unitPriceInclTaxValue, _) = await _taxService.GetProductPriceAsync(product, unitPrice, true, customer); - var (unitPriceExclTaxValue, _) = await _taxService.GetProductPriceAsync(product, unitPriceInclTaxValue, false, customer); - + // Calculate tax + //var (unitPriceInclTaxValue, _) = await _taxService.GetProductPriceAsync(product, unitPrice, true, customer); + var (unitPriceExclTaxValue, _) = await _taxService.GetProductPriceAsync(product, unitPriceInclTaxValue, false, customer); + return new OrderItem { OrderId = order.Id, @@ -664,7 +665,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers PriceInclTax = isMeasurable ? 0 : unitPriceInclTaxValue * orderProductItem.Quantity, PriceExclTax = isMeasurable ? 0 : unitPriceExclTaxValue * orderProductItem.Quantity, - + OriginalProductCost = await _priceCalculationService.GetProductCostAsync(product, null), AttributeDescription = string.Empty, @@ -936,7 +937,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers var productDto = productDtosById[product.Id]; if (productDto != null) { - if(productDto.AvailableQuantity > 0) + if (productDto.AvailableQuantity > 0) { result.Add(new { @@ -954,6 +955,46 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return Json(result); } + [HttpGet] + [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] + public virtual async Task ProductSearchUnfilteredAutoComplete(string term) + { + if (string.IsNullOrWhiteSpace(term) || term.Length < 2) + return Json(new List()); + + const int maxResults = 30; + + // Search products by name or SKU + var products = await _productService.SearchProductsAsync( + keywords: term, + pageIndex: 0, + pageSize: maxResults); + + var result = new List(); + var productDtosById = await _dbContext.ProductDtos.GetAllByIds(products.Select(p => p.Id)).ToDictionaryAsync(k => k.Id, v => v); + + foreach (var product in products) + { + var productDto = productDtosById[product.Id]; + if (productDto != null) + { + + result.Add(new + { + label = $"{product.Name} [RENDELHETŐ: {(product.StockQuantity + productDto.IncomingQuantity)}] [ÁR: {product.Price}]", + value = product.Id, + sku = product.Sku, + price = product.Price, + stockQuantity = product.StockQuantity, + incomingQuantity = productDto.IncomingQuantity, + }); + + } + } + + return Json(result); + } + //[HttpPost] //public async Task CreateInvoice(int orderId) //{ @@ -1362,21 +1403,21 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers if (!await _permissionService.AuthorizeAsync(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)) return Json(new { success = false, message = "Access denied" }); - + if (string.IsNullOrEmpty(productsJson)) return Json(new { success = false, message = "No products data received" }); - + var order = await _orderService.GetOrderByIdAsync(orderId); if (order == null || order.Deleted) return Json(new { success = false, message = "Order not found" }); - + // Deserialize products var products = productsJson.JsonTo>(); //JsonConvert.DeserializeObject>(productsJson); if (products == null || products.Count == 0) return Json(new { success = false, message = "No products to add" }); - + var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId); var store = await _storeContext.GetCurrentStoreAsync(); var admin = await _workContext.GetCurrentCustomerAsync(); @@ -1409,6 +1450,272 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers return Json(new { success = false, message = $"Error: {ex.Message}" }); } } + + [HttpPost] + [ValidateAntiForgeryToken] + [CheckPermission(StandardPermission.Orders.ORDERS_CREATE_EDIT_DELETE)] + public async Task SplitOrder(int orderId) + { + try + { + _logger.Info($"SplitOrder - OrderId: {orderId} - STARTED"); + + var order = await _orderService.GetOrderByIdAsync(orderId); + if (order == null || order.Deleted) + { + _logger.Warning($"SplitOrder - Order {orderId} not found or deleted"); + return Json(new { success = false, message = "Rendelés nem található" }); + } + + _logger.Info($"SplitOrder - Order {orderId} found, checking access"); + + // Check if user has access to this order + if (!await HasAccessToOrderAsync(order)) + { + _logger.Warning($"SplitOrder - No access to order {orderId}"); + return Json(new { success = false, message = "Nincs jogosultsága ehhez a rendeléshez" }); + } + + _logger.Info($"SplitOrder - Getting OrderDto for order {orderId}"); + + var orderDto = await _dbContext.OrderDtos.GetByIdAsync(orderId, true); + if (orderDto == null) + { + _logger.Warning($"SplitOrder - OrderDto not found for order {orderId}"); + return Json(new { success = false, message = "OrderDto nem található" }); + } + + // SAFETY CHECK: Don't allow splitting if order is complete/audited + if (orderDto.MeasuringStatus == MeasuringStatus.Audited) + { + _logger.Warning($"SplitOrder - Cannot split audited order {orderId}"); + return Json(new + { + success = false, + message = "Ez a rendelés már auditált, nem választható szét!" + }); + } + + // SAFETY CHECK: Don't allow splitting if measuring hasn't started + if (orderDto.MeasuringStatus == MeasuringStatus.NotStarted) + { + _logger.Warning($"SplitOrder - Cannot split order {orderId} that hasn't been started"); + return Json(new + { + success = false, + message = "Ez a rendelés még nem lett elkezdve, nem választható szét!" + }); + } + + _logger.Info($"SplitOrder - OrderDto found, separating items. Total items: {orderDto.OrderItemDtos.Count}, MeasuringStatus: {orderDto.MeasuringStatus}"); + + // Separate audited and non-audited items + var auditedItems = orderDto.OrderItemDtos.Where(oi => oi.IsAudited).ToList(); + var nonAuditedItems = orderDto.OrderItemDtos.Where(oi => !oi.IsAudited).ToList(); + + _logger.Info($"SplitOrder - Audited items: {auditedItems.Count}, Non-audited items: {nonAuditedItems.Count}"); + + if (nonAuditedItems.Count == 0) + { + _logger.Warning($"SplitOrder - No non-audited items in order {orderId}"); + return Json(new + { + success = false, + message = "Nincs nem auditált termék a rendelésben. Szétválasztás nem szükséges." + }); + } + + if (auditedItems.Count == 0) + { + _logger.Warning($"SplitOrder - All items are non-audited in order {orderId}"); + return Json(new + { + success = false, + message = "Minden termék nem auditált. Szétválasztás nem szükséges." + }); + } + + _logger.Info($"SplitOrder - Getting customer, store, and admin"); + + var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId); + var store = await _storeContext.GetCurrentStoreAsync(); + var admin = await _workContext.GetCurrentCustomerAsync(); + + _logger.Info($"SplitOrder - Customer: {customer?.Id}, Store: {store?.Id}, Admin: {admin?.Id}"); + _logger.Info($"SplitOrder - Creating new order (no transaction)"); + + // Create new order for non-audited items + var newOrder = new Order + { + OrderGuid = Guid.NewGuid(), + CustomOrderNumber = "", + CustomerId = order.CustomerId, + CustomerLanguageId = order.CustomerLanguageId, + CustomerTaxDisplayType = order.CustomerTaxDisplayType, + CustomerIp = order.CustomerIp, + OrderStatus = OrderStatus.Pending, + PaymentStatus = PaymentStatus.Pending, + ShippingStatus = ShippingStatus.ShippingNotRequired, + CreatedOnUtc = DateTime.UtcNow, + BillingAddressId = order.BillingAddressId, + ShippingAddressId = order.ShippingAddressId, + PaymentMethodSystemName = order.PaymentMethodSystemName, + CustomerCurrencyCode = order.CustomerCurrencyCode, + OrderTotal = 0, + OrderSubtotalInclTax = 0, + OrderSubtotalExclTax = 0, + OrderSubTotalDiscountInclTax = 0, + OrderSubTotalDiscountExclTax = 0, + }; + + _logger.Info($"SplitOrder - Inserting new order"); + await _orderService.InsertOrderAsync(newOrder); + + _logger.Info($"SplitOrder - New order inserted with ID: {newOrder.Id}"); + newOrder.CustomOrderNumber = newOrder.Id.ToString(); + await _orderService.UpdateOrderAsync(newOrder); + + // Get original order items + _logger.Info($"SplitOrder - Getting original order items"); + var originalOrderItems = await _orderService.GetOrderItemsAsync(orderId); + _logger.Info($"SplitOrder - Found {originalOrderItems.Count} original order items"); + + var orderItemsToMove = new List(); + + // Find order items to move based on non-audited OrderItemDtos + foreach (var nonAuditedDto in nonAuditedItems) + { + var orderItemToMove = originalOrderItems.FirstOrDefault(oi => oi.Id == nonAuditedDto.Id); + if (orderItemToMove != null) + { + orderItemsToMove.Add(orderItemToMove); + } + } + + _logger.Info($"SplitOrder - Found {orderItemsToMove.Count} items to move"); + + // Move non-audited items to new order + foreach (var orderItem in orderItemsToMove) + { + _logger.Info($"SplitOrder - Processing order item {orderItem.Id}"); + + var product = await _productService.GetProductByIdAsync(orderItem.ProductId); + if (product == null) + { + _logger.Warning($"Product with ID {orderItem.ProductId} not found during split"); + continue; + } + + // Create new order item for new order + var newOrderItem = new OrderItem + { + OrderId = newOrder.Id, + ProductId = orderItem.ProductId, + Quantity = orderItem.Quantity, + OrderItemGuid = Guid.NewGuid(), + UnitPriceInclTax = orderItem.UnitPriceInclTax, + UnitPriceExclTax = orderItem.UnitPriceExclTax, + PriceInclTax = orderItem.PriceInclTax, + PriceExclTax = orderItem.PriceExclTax, + OriginalProductCost = orderItem.OriginalProductCost, + AttributeDescription = orderItem.AttributeDescription, + AttributesXml = orderItem.AttributesXml, + DiscountAmountInclTax = orderItem.DiscountAmountInclTax, + DiscountAmountExclTax = orderItem.DiscountAmountExclTax, + DownloadCount = 0, + IsDownloadActivated = false, + LicenseDownloadId = 0, + ItemWeight = orderItem.ItemWeight, + RentalStartDateUtc = orderItem.RentalStartDateUtc, + RentalEndDateUtc = orderItem.RentalEndDateUtc + }; + + _logger.Info($"SplitOrder - Inserting new order item for product {orderItem.ProductId}"); + await _orderService.InsertOrderItemAsync(newOrderItem); + + // Update new order totals + newOrder.OrderSubtotalInclTax += newOrderItem.PriceInclTax; + newOrder.OrderSubtotalExclTax += newOrderItem.PriceExclTax; + newOrder.OrderTotal += newOrderItem.PriceInclTax; + + _logger.Info($"SplitOrder - Adjusting inventory for product {orderItem.ProductId}"); + + // Return inventory to stock (from original order) + await _productService.AdjustInventoryAsync( + product, + orderItem.Quantity, + orderItem.AttributesXml, + $"Returned from split order #{order.Id}" + ); + + // Remove from stock (for new order) + await _productService.AdjustInventoryAsync( + product, + -orderItem.Quantity, + orderItem.AttributesXml, + $"Split to new order #{newOrder.Id}" + ); + + _logger.Info($"SplitOrder - Deleting order item {orderItem.Id} from original order"); + + // Delete from original order + await _orderService.DeleteOrderItemAsync(orderItem); + + // Update original order totals + order.OrderSubtotalInclTax -= orderItem.PriceInclTax; + order.OrderSubtotalExclTax -= orderItem.PriceExclTax; + order.OrderTotal -= orderItem.PriceInclTax; + } + + _logger.Info($"SplitOrder - Updating both orders"); + + // Update both orders + await _orderService.UpdateOrderAsync(newOrder); + await _orderService.UpdateOrderAsync(order); + + _logger.Info($"SplitOrder - Adding order notes"); + + // Add notes to both orders + await InsertOrderNoteAsync( + order.Id, + false, + $"Rendelés szétválasztva. Nem auditált termékek átkerültek a #{newOrder.Id} rendelésbe. Művelet végrehajtója: {admin.FirstName} {admin.LastName} (ID: {admin.Id})" + ); + + await InsertOrderNoteAsync( + newOrder.Id, + false, + $"Új rendelés létrehozva a #{order.Id} rendelés szétválasztásával. Nem auditált termékek. Művelet végrehajtója: {admin.FirstName} {admin.LastName} (ID: {admin.Id})" + ); + + _logger.Info($"Order {orderId} split successfully. New order created: {newOrder.Id}. Moved {orderItemsToMove.Count} items."); + + // Send notifications + _logger.Info($"SplitOrder - Sending notifications"); + + var originalOrderDto = await _dbContext.OrderDtos.GetByIdAsync(order.Id, true); + var newOrderDto = await _dbContext.OrderDtos.GetByIdAsync(newOrder.Id, true); + + await _sendToClient.SendOrderChanged(originalOrderDto); + await _sendToClient.SendOrderChanged(newOrderDto); + + _logger.Info($"SplitOrder - COMPLETED SUCCESSFULLY"); + + return Json(new + { + success = true, + message = "Rendelés sikeresen szétválasztva", + newOrderId = newOrder.Id, + originalOrderId = order.Id + }); + } + catch (Exception ex) + { + _logger.Error($"Error splitting order {orderId}: {ex.Message}", ex); + return Json(new { success = false, message = $"Hiba: {ex.Message}" }); + } + } + } } diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerController.cs index de64b27..75322fd 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerController.cs +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileManagerController.cs @@ -1,11 +1,18 @@ -using FruitBank.Common.Dtos; + +using DocumentFormat.OpenXml.Vml; +using FruitBank.Common; +using FruitBank.Common.Dtos; using FruitBank.Common.Entities; +using FruitBank.Common.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Helpers; using Nop.Plugin.Misc.FruitBankPlugin.Services; +using Nop.Plugin.Misc.FruitBankPlugin.Services.FileStorage; using Nop.Services.Catalog; using Nop.Services.Security; using Nop.Web.Framework; @@ -28,6 +35,8 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers private readonly IProductService _productService; private readonly FruitBankDbContext _dbContext; private readonly PdfToImageService _pdfToImageService; + private readonly IWorkContext _workContext; + private readonly FileStorageService _fileStorageService; public FileManagerController( IPermissionService permissionService, @@ -35,7 +44,9 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers AICalculationService aiCalculationService, IProductService productService, FruitBankDbContext fruitBankDbContext, - PdfToImageService pdfToImageService) + PdfToImageService pdfToImageService, + IWorkContext workContext, + FileStorageService fileStorageService) { _permissionService = permissionService; _aiApiService = aiApiService; @@ -43,6 +54,8 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers _productService = productService; _dbContext = fruitBankDbContext; _pdfToImageService = pdfToImageService; + _workContext = workContext; + _fileStorageService = fileStorageService; } /// @@ -57,7 +70,8 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers } /// - /// Endpoint to extract text from uploaded image + /// Endpoint to extract text from uploaded image with duplicate detection + /// FIXED: Removed EF Core .Include() dependency /// [HttpPost] public async Task ExtractTextFromImage(IFormFile imageFile, string customPrompt = null) @@ -71,20 +85,114 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers } // Validate file type - now including PDF - var extension = Path.GetExtension(imageFile.FileName).ToLowerInvariant(); + var extension = System.IO.Path.GetExtension(imageFile.FileName).ToLowerInvariant(); if (extension != ".jpg" && extension != ".jpeg" && extension != ".png" && extension != ".gif" && extension != ".webp" && extension != ".pdf") { return Json(new { success = false, message = "Invalid file type. Please upload JPG, PNG, GIF, WebP, or PDF." }); } - ShippingDocument shippingDocument = new ShippingDocument(); - shippingDocument.ShippingItems = new List(); - try { + // ✅ STEP 1: Calculate file hash FIRST to check for duplicates + string fileHash; + using (var hashStream = imageFile.OpenReadStream()) + { + using (var sha256 = System.Security.Cryptography.SHA256.Create()) + { + var hashBytes = await Task.Run(() => sha256.ComputeHash(hashStream)); + fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + } + + Console.WriteLine($"📝 Uploaded file hash: {fileHash}"); + + // ✅ STEP 2: Check if this file has already been processed + var existingFile = await _dbContext.Files + .GetAll() + .FirstOrDefaultAsync(f => f.FileHash == fileHash); + + if (existingFile != null) + { + Console.WriteLine($"♻️ Duplicate file detected! File ID: {existingFile.Id}"); + + // Find the shipping document(s) associated with this file + var existingMappingList = await _dbContext.ShippingDocumentToFiles + .GetAll() + .Where(m => m.FilesId == existingFile.Id) + .OrderByDescending(m => m.Created).ToListAsync(); + var existingMapping = existingMappingList.FirstOrDefault(); + + if (existingMapping != null) + { + // ✅ Load the existing shipping document (without .Include()) + var existingShippingDoc = await _dbContext.ShippingDocuments + .GetByIdAsync(existingMapping.ShippingDocumentId); + + if (existingShippingDoc != null) + { + Console.WriteLine($"✅ Found existing shipping document - ID: {existingShippingDoc.Id}"); + + // ✅ Manually load related data + var shippingItems = await _dbContext.ShippingItems + .GetAll() + .Where(si => si.ShippingDocumentId == existingShippingDoc.Id) + .ToListAsync(); + + Partner matchedPartner = null; + if (existingShippingDoc.PartnerId > 0) + { + matchedPartner = await _dbContext.Partners.GetByIdAsync(existingShippingDoc.PartnerId); + } + + // Return the existing data instead of re-processing + return Json(new + { + success = true, + isDuplicate = true, // ✅ Flag so frontend can show warning + message = $"This document was already processed on {existingShippingDoc.ShippingDate.ToString("yyyy-MM-dd HH:mm")}. Loaded existing data.", + existingDocumentId = existingShippingDoc.Id, + shippingDocument = new + { + documentIdNumber = existingShippingDoc.DocumentIdNumber, + partnerId = existingShippingDoc.PartnerId, + partnerName = matchedPartner?.Name, + partnerTaxId = matchedPartner?.TaxId, + pdfFileName = existingShippingDoc.PdfFileName, + totalPallets = existingShippingDoc.TotalPallets, + shippingItems = shippingItems?.Select(item => new + { + name = item.Name, + hungarianName = item.HungarianName, + nameOnDocument = item.NameOnDocument, + productId = item.ProductId, + palletsOnDocument = item.PalletsOnDocument, + quantityOnDocument = item.QuantityOnDocument, + netWeightOnDocument = item.NetWeightOnDocument, + grossWeightOnDocument = item.GrossWeightOnDocument, + unitPriceOnDocument = item.UnitPriceOnDocument + }).ToList(), + extractedText = existingFile.RawText + }, + fileName = existingFile.FileName + existingFile.FileExtension, + fileSize = imageFile.Length, + wasConverted = extension == ".pdf" + }); + } + } + + // If we found the file but no shipping document, continue with extraction + Console.WriteLine("⚠️ File exists but no shipping document found. Continuing with extraction..."); + } + + // ✅ STEP 3: File is new or has no associated document - proceed with extraction + Console.WriteLine("🆕 New file detected. Starting AI extraction..."); + + ShippingDocument shippingDocument = new ShippingDocument(); + shippingDocument.ShippingItems = new List(); + // Define the uploads folder - var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "ocr"); + var uploadsFolder = System.IO.Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "ocr"); // Create directory if it doesn't exist if (!Directory.Exists(uploadsFolder)) @@ -100,7 +208,7 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers { // Save the PDF temporarily var tempPdfFileName = $"temp_pdf_{DateTime.Now:yyyyMMdd_HHmmss}.pdf"; - var tempPdfPath = Path.Combine(uploadsFolder, tempPdfFileName); + var tempPdfPath = System.IO.Path.Combine(uploadsFolder, tempPdfFileName); using (var stream = new FileStream(tempPdfPath, FileMode.Create)) { @@ -121,7 +229,7 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers // Use the first page processedFilePath = convertedImages[0]; - processedFileName = Path.GetFileName(processedFilePath); + processedFileName = System.IO.Path.GetFileName(processedFilePath); // Clean up temp PDF if (System.IO.File.Exists(tempPdfPath)) @@ -131,7 +239,7 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers { // Handle regular image files processedFileName = $"ocr_image_{DateTime.Now:yyyyMMdd_HHmmss}{extension}"; - processedFilePath = Path.Combine(uploadsFolder, processedFileName); + processedFilePath = System.IO.Path.Combine(uploadsFolder, processedFileName); using (var stream = new FileStream(processedFilePath, FileMode.Create)) { @@ -158,14 +266,15 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers message = "Failed to extract text. The API may have returned an empty response." }); } + OpenaiImageResponse deserializedContent = new(); var result = TextHelper.FixJsonWithoutAI(extractedText); var options = new System.Text.Json.JsonSerializerOptions { - PropertyNameCaseInsensitive = true, // Handles camelCase/PascalCase mismatches - IncludeFields = true // This allows deserializing fields (in case you keep it as a field) + PropertyNameCaseInsensitive = true, + IncludeFields = true }; try @@ -183,15 +292,11 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers Console.Error.WriteLine($"JSON content: {result}"); } - - //string documentIdAnalysisResult = await ExtractDocumentId(deserializedContent.extractedData.fullText); - Console.WriteLine($"Document number analysis Result: {deserializedContent.extractedData.documentId}"); shippingDocument.DocumentIdNumber = deserializedContent.extractedData.documentId; string partnerAnalysis = await ExtractPartnerName(extractedText); - //int? dbPartnerName = await DeterminePartner(deserializedContent.extractedData.partner.name); int? dbPartnerName = await DeterminePartner(partnerAnalysis); if (dbPartnerName != null) { @@ -203,21 +308,17 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers Console.WriteLine("No matching partner found in the database."); } - //string productAnalysis = await _aiCalculationService.ExtractProducts(extractedText); Console.WriteLine($"Product analysis Result: {deserializedContent.extractedData.products}"); - //identify products from database + // Identify products from database var allProducts = await _dbContext.ProductDtos.GetAll(true).ToListAsync(); var historicalProducts = await _dbContext.ShippingItems.GetAll().ToListAsync(); - //create json from product analyzis jsonstring ProductReferenceResponse deserializedProducts = new ProductReferenceResponse(); - //deserializedProducts.products = new List(); deserializedProducts.products = deserializedContent.extractedData.products; Console.WriteLine($"Serialized Products: {deserializedProducts.products.Count}"); List matchedProducts = new List(); - //do we have historical references? matchedProducts = await DetermineProducts(allProducts, historicalProducts, deserializedProducts); shippingDocument.ShippingItems = matchedProducts; @@ -236,27 +337,21 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers } shippingDocument.PdfFileName = processedFileName; - shippingDocument.ShippingDocumentToFiles = new List(); - - Files processedFile = new Files - { - FileName = processedFileName, - FileExtension = extension, - RawText = deserializedContent.extractedData.fullText, - }; - - ShippingDocumentToFiles shippingDocumentToFiles = new ShippingDocumentToFiles - { - FilesId = processedFile.Id, - ShippingDocumentId = shippingDocument.Id, - }; // Calculate total pallets from shipping items shippingDocument.TotalPallets = shippingDocument.ShippingItems?.Sum(item => item.PalletsOnDocument) ?? 0; + // Get partner details if matched + Partner matchedPartner2 = null; + if (shippingDocument.PartnerId > 0) + { + matchedPartner2 = await _dbContext.Partners.GetByIdAsync(shippingDocument.PartnerId); + } + return Json(new { success = true, + isDuplicate = false, // ✅ Flag indicating this is new extraction message = extension == ".pdf" ? "PDF converted and text extracted successfully" : "Text extracted successfully", @@ -264,6 +359,8 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers { documentIdNumber = shippingDocument.DocumentIdNumber, partnerId = shippingDocument.PartnerId, + partnerName = matchedPartner2?.Name, + partnerTaxId = matchedPartner2?.TaxId, pdfFileName = shippingDocument.PdfFileName, totalPallets = shippingDocument.TotalPallets, shippingItems = shippingDocument.ShippingItems?.Select(item => new @@ -276,7 +373,7 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers quantityOnDocument = item.QuantityOnDocument, netWeightOnDocument = item.NetWeightOnDocument, grossWeightOnDocument = item.GrossWeightOnDocument, - isMeasurable = item.IsMeasurable + unitPriceOnDocument = item.UnitPriceOnDocument }).ToList(), extractedText = deserializedContent.extractedData.fullText }, @@ -586,87 +683,6 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers return cleaned; } - private async Task TestFullResult(string extractedText) - { - string fullResultPrompt = $"Role:\r\nYou are an AI data extraction assistant for Fruitbank, a " + - $"fruit and vegetable wholesale company. Your task is to analyze a " + - $"provided text (delivery notes, invoices, or order confirmations) and extract structured information about " + - $"the shipment and its items.\r\n\r\n🎯 Goal:\r\nRead the provided text and extract all shipment " + - $"details and items according to the data model below.\r\n Generate the complete JSON output following this " + - $"structure.\r\n\r\n🧩 Data Models:\r\n\r\npublic " + - $"class Partner\r\n{{\r\n " + - $"/// \r\n /// Partner entity primary key\r\n /// \r\n " + - $"public int Id {{ get; set; }}\r\n " + - $"/// \r\n /// Partner company name\r\n /// \r\n " + - $"public string Name {{ get; set; }}\r\n " + - $"/// \r\n /// Partner company TaxId\r\n /// \r\n " + - $"public string TaxId {{ get; set; }}\r\n /// \r\n " + - $"/// Partner company Certification if exists\r\n /// \r\n " + - $"public string CertificationNumber {{ get; set; }}\r\n /// \r\n " + - $"/// Partner company address PostalCode\r\n /// \r\n " + - $"public string PostalCode {{ get; set; }}\r\n /// \r\n " + - $"/// Partner company address Country\r\n /// \r\n " + - $"public string Country {{ get; set; }}\r\n /// \r\n " + - $"/// Partner company address State if exists\r\n /// \r\n " + - $"public string State {{ get; set; }}\r\n /// \r\n " + - $"/// Partner company address County if exists\r\n /// \r\n " + - $"public string County {{ get; set; }}\r\n /// \r\n " + - $"/// Partner company address City\r\n /// \r\n " + - $"public string City {{ get; set; }}\r\n /// \r\n " + - $"/// Partner company address Street\r\n /// \r\n " + - $"public string Street {{ get; set; }}\r\n\t/// \r\n " + - $"/// Entities of ShippingDocument\r\n /// \r\n\tpublic List " + - $"ShippingDocuments {{ get; set; }}\t\r\n}}\r\n\r\npublic class ShippingDocument\r\n{{\r\n " + - $"/// \r\n /// ShippingItem entity primary key\r\n /// \r\n " + - $"public int Id {{ get; set; }}\r\n /// \r\n /// Partner entity primary key\r\n " + - $"/// \r\n public int PartnerId {{ get; set; }}\t\r\n\t/// \r\n " + - $"/// Entities of ShippingItem\r\n /// \r\n\t" + - $"public List ShippingItems {{ get; set; }}\r\n /// \r\n " + - $"/// DocumentIdNumber if exists\r\n /// \r\n public string DocumentIdNumber {{ get; set; }}\r\n " + - $"/// \r\n /// \r\n /// \r\n public DateTime ShippingDate {{ get; set; }}\r\n " + - $"/// \r\n /// Shipping pickup Contry of origin\r\n /// \r\n " + - $"public string Country {{ get; set; }}\r\n\t/// \r\n /// Sum of ShippingItem pallets\r\n " + - $"/// \r\n public int TotalPallets {{ get; set; }}\r\n\t/// \r\n " + - $"/// Filename of pdf\r\n /// \r\n\tpublic string PdfFileName {{ get; set; }}\r\n}}\r\n\r\n" + - $"public class ShippingItem\r\n{{\r\n /// \r\n /// ShippingItem entity primary key\r\n /// " + - $"\r\n public int Id {{ get; set; }}\r\n /// \r\n /// " + - $"ShippingDocument entity primary key\r\n /// \r\n " + - $"public int ShippingDocumentId {{ get; set; }}\r\n /// " + - $"\r\n /// Name of the fruit or vegitable\r\n /// \r\n " + - $"public string Name {{ get; set; }}\r\n\t/// \r\n /// Translated Name to Hungarian\r\n " + - $"/// \r\n public string HungarianName {{ get; set; }}\r\n /// \r\n " + - $"/// Pallets of fruit or vegitable item\r\n /// \r\n " + - $"public int PalletsOnDocument {{ get; set; }}\r\n /// \r\n " + - $"/// Quantity of fruit or vegitable item\r\n /// \r\n " + - $"public int QuantityOnDocument {{ get; set; }}\r\n /// \r\n " + - $"/// Net weight in kg. of fruit or vegitable item\r\n /// \r\n " + - $"public double NetWeightOnDocument {{ get; set; }}\r\n /// \r\n " + - $"/// Gross weight in kg. of fruit or vegitable item\r\n /// \r\n " + - $"public double GrossWeightOnDocument {{ get; set; }}\r\n}}\r\n\r\n🧾 Output Requirements\r\n- " + - $"Output must be a single valid JSON object containing:\r\n- One Partner object\r\n- " + - $"One ShippingDocument object\r\n- A list of all related ShippingItem objects\r\n\r\n- " + - $"Primary keys (Partner.Id, ShippingDocument.Id, ShippingItem.Id) should be auto-generated integers " + - $"(e.g. sequential: 1, 2, 3…).\r\n\r\n- When a field is missing or unclear, return it as an empty " + - $"string or 0 (depending on type).\r\nDo not omit any fields.\r\n\r\n- " + - $"All dates must be in ISO 8601 format (yyyy-MM-dd).\r\n\r\n🧭 Instructions to the AI\r\n" + - $"1. Analyze the provided text carefully.\r\n" + - $"2. Identify the Partner/Company details of THE OTHER PARTY (other than Fruitbank), " + - $"document identifiers, and each shipment item.\r\n" + - $"3. FruitBank is not a partner! Always look for THE OTHER partner on the document. \r\n " + - $"4. Generate a complete hierarchical JSON of ALL received documents in ONE JSON structure according to the " + - $"data model above.\r\n5. Do not include any explanations or text outside the JSON output. " + - $"Only return the structured JSON.\r\n" + - $"6. A teljes ShippingItem.Name-et tedd bele a ShippingItem.HungarianName-be " + - $"és a zöldség vagy gyümölcs nevét fordítsd le magyarra!\r\n" + - $"7. A ShippingDocument-et tedd bele a Partner entitásba!\r\n" + - $"8. ShippingItem-eket tedd bele a ShippingDocument-be!\r\n" + - $"9. Do not assume or modify any data, if you don't find a value, return null, if you find a value, keep it unmodified.\r\n" + - $"10. Magyarázat nélkül válaszolj!"; - - var fullresult = await _aiApiService.GetSimpleResponseAsync(fullResultPrompt, extractedText); - return fullresult; - } - private async Task DeterminePartner(string partnerAnalysis) { // Clean the input first @@ -887,8 +903,214 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers // //analyze the text for document number or identifiers // return await _aiApiService.GetSimpleResponseAsync("You are an agent of Fruitbank to analyize text extracted frem a pfd document, and find the document number or identifier. IMPORTANT: reply only with the number, do not add further explanation.", $"What is the document identifier of this document: {extractedText}"); //} + + /// + /// Partner search autocomplete endpoint for finding partners by name or tax ID + /// + [HttpGet] + public async Task PartnerSearchAutoComplete(string term) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + if (string.IsNullOrWhiteSpace(term) || term.Length < 2) + return Json(new List()); + + try + { + const int maxResults = 20; + + // Get all partners and filter in-memory (or create a custom search method) + var allPartners = await _dbContext.Partners.GetAll().ToListAsync(); + + var matchedPartners = allPartners + .Where(p => + (!string.IsNullOrEmpty(p.Name) && p.Name.Contains(term, StringComparison.OrdinalIgnoreCase)) || + (!string.IsNullOrEmpty(p.TaxId) && p.TaxId.Contains(term, StringComparison.OrdinalIgnoreCase)) + ) + .Take(maxResults) + .ToList(); + + var result = matchedPartners.Select(partner => new + { + label = $"{partner.Name} - Tax ID: {partner.TaxId ?? "N/A"}", + value = partner.Id, + name = partner.Name, + taxId = partner.TaxId, + city = partner.City, + country = partner.Country + }).ToList(); + + return Json(result); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error in PartnerSearchAutoComplete: {ex}"); + return Json(new List()); + } + } + + /// + /// Save a shipping document with its items AND the original uploaded file + /// + [HttpPost] + public async Task SaveShippingDocument( + [FromForm] string documentData, + [FromForm] IFormFile originalFile, + [FromForm] string extractedText) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + if (string.IsNullOrEmpty(documentData)) + { + return Json(new { success = false, message = "Invalid request data" }); + } + + try + { + // ✅ Configure JSON options to handle camelCase from JavaScript + var jsonOptions = new System.Text.Json.JsonSerializerOptions + { + PropertyNameCaseInsensitive = true // This handles camelCase <-> PascalCase + }; + + // Deserialize the shipping document data + var request = System.Text.Json.JsonSerializer.Deserialize(documentData, jsonOptions); + + if (request == null) + { + return Json(new { success = false, message = "Failed to parse document data" }); + } + + // Validate required fields + if (string.IsNullOrEmpty(request.DocumentIdNumber)) + { + return Json(new { success = false, message = "Document ID is required" }); + } + + if (!request.PartnerId.HasValue || request.PartnerId <= 0) + { + return Json(new { success = false, message = "Valid Partner ID is required" }); + } + + if (request.ShippingItems == null || !request.ShippingItems.Any()) + { + return Json(new { success = false, message = "At least one shipping item is required" }); + } + + var partner = await _dbContext.Partners.GetByIdAsync(request.PartnerId.Value); + + // Create the shipping document entity + var shippingDocument = new ShippingDocument + { + DocumentIdNumber = request.DocumentIdNumber, + PartnerId = request.PartnerId.Value, + Country = partner?.Country, + TotalPallets = request.TotalPallets, + PdfFileName = originalFile.FileName, + ShippingDate = DateTime.UtcNow, + ShippingItems = new List() + }; + + // Convert DTOs to entities + foreach (var itemDto in request.ShippingItems) + { + var productDto = await _dbContext.ProductDtos.GetByIdAsync(itemDto.ProductId ?? 0, true); + if (productDto != null && string.IsNullOrEmpty(itemDto.Name)) + { + itemDto.IsMeasurable = productDto.IsMeasurable; + } + var shippingItem = new ShippingItem + { + Name = itemDto.Name, + HungarianName = itemDto.HungarianName, + NameOnDocument = itemDto.NameOnDocument, + ProductId = itemDto.ProductId, + PalletsOnDocument = itemDto.PalletsOnDocument, + QuantityOnDocument = itemDto.QuantityOnDocument, + NetWeightOnDocument = itemDto.NetWeightOnDocument, + GrossWeightOnDocument = itemDto.GrossWeightOnDocument, + UnitPriceOnDocument = itemDto.UnitPriceOnDocument, + IsMeasurable = itemDto.IsMeasurable + }; + + shippingDocument.ShippingItems.Add(shippingItem); + } + + // ✅ STEP 1: Save shipping document to database FIRST + await _dbContext.ShippingDocuments.InsertAsync(shippingDocument); + foreach (var sItem in shippingDocument.ShippingItems) + { + sItem.ShippingDocumentId = shippingDocument.Id; // Set FK + await _dbContext.ShippingItems.InsertAsync(sItem); + } + + Console.WriteLine($"✓ Shipping document saved - ID: {shippingDocument.Id}, Document ID: {shippingDocument.DocumentIdNumber}"); + + // ✅ STEP 2: NOW save the original file (only if document saved successfully) + if (originalFile != null && originalFile.Length > 0) + { + var currentUser = await _workContext.GetCurrentCustomerAsync(); + + using (var fileStream = originalFile.OpenReadStream()) + { + var savedFile = await _fileStorageService.SaveFileAsync( + fileStream: fileStream, + fileName: originalFile.FileName, + userId: currentUser.Id, + featureName: "ShippingDocumentProcessing", + entityType: "ShippingDocuments", + entityId: shippingDocument.Id, // ✅ Now we have the real ID! + rawText: extractedText, + checkForDuplicates: true + ); + + // Create mapping between ShippingDocument and File + var mapping = new ShippingDocumentToFiles + { + FilesId = savedFile.Id, + ShippingDocumentId = shippingDocument.Id, + DocumentTypeId = (int)DocumentType.ShippingDocument, + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow + }; + + await _dbContext.ShippingDocumentToFiles.InsertAsync(mapping); + + + Console.WriteLine($"✓ Original file saved - File ID: {savedFile.Id}, Filename: {savedFile.FileName}"); + } + } + else + { + Console.WriteLine("⚠ No original file provided - skipping file save"); + } + + return Json(new + { + success = true, + message = "Shipping document and file saved successfully", + shippingDocumentId = shippingDocument.Id, + documentId = shippingDocument.DocumentIdNumber, + itemCount = shippingDocument.ShippingItems.Count + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error saving shipping document: {ex}"); + return Json(new + { + success = false, + message = $"Error saving document: {ex.Message}" + }); + } + } + } + + public class ProductReference { public string? name { get; set; } @@ -904,4 +1126,27 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers public List products { get; set; } } + public class SaveShippingDocumentRequest + { + public string DocumentIdNumber { get; set; } + public int? PartnerId { get; set; } + public int TotalPallets { get; set; } + public string PdfFileName { get; set; } + public List ShippingItems { get; set; } + } + + public class SaveShippingItemDto + { + public string Name { get; set; } + public string HungarianName { get; set; } + public string NameOnDocument { get; set; } + public int? ProductId { get; set; } + public int PalletsOnDocument { get; set; } + public int QuantityOnDocument { get; set; } + public double NetWeightOnDocument { get; set; } + public double GrossWeightOnDocument { get; set; } + public double UnitPriceOnDocument { get; set; } + public bool IsMeasurable { get; set; } + } + } diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileStorageController.cs b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileStorageController.cs new file mode 100644 index 0000000..a1653a5 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/FileStorageController.cs @@ -0,0 +1,684 @@ +using FruitBank.Common.Entities; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Nop.Core; +using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; +using Nop.Plugin.Misc.FruitBankPlugin.Services.FileStorage; +using Nop.Services.Security; +using Nop.Web.Framework; +using Nop.Web.Framework.Controllers; +using Nop.Web.Framework.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Nop.Plugin.Misc.FruitBank.Controllers +{ + [AuthorizeAdmin] + [Area(AreaNames.ADMIN)] + [AutoValidateAntiforgeryToken] + public class FileStorageController : BasePluginController + { + private readonly FileStorageService _fileStorageService; + private readonly FruitBankDbContext _dbContext; + private readonly IPermissionService _permissionService; + private readonly IWorkContext _workContext; + + public FileStorageController( + FileStorageService fileStorageService, + FruitBankDbContext dbContext, + IPermissionService permissionService, + IWorkContext workContext) + { + _fileStorageService = fileStorageService; + _dbContext = dbContext; + _permissionService = permissionService; + _workContext = workContext; + } + + #region Upload Files + + /// + /// Upload a single file + /// + /// The uploaded file + /// Feature name (e.g., "AIdocumentprocessing") + /// Entity type (e.g., "ShippingDocuments") + /// Entity ID + /// Optional raw text for searchable documents + [HttpPost] + public async Task UploadFile( + IFormFile file, + string featureName, + string entityType, + int entityId, + string rawText = null) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + if (file == null || file.Length == 0) + return Json(new { success = false, message = "No file uploaded" }); + + if (string.IsNullOrWhiteSpace(featureName)) + return Json(new { success = false, message = "Feature name is required" }); + + if (string.IsNullOrWhiteSpace(entityType)) + return Json(new { success = false, message = "Entity type is required" }); + + if (entityId <= 0) + return Json(new { success = false, message = "Valid entity ID is required" }); + + try + { + var currentUser = await _workContext.GetCurrentCustomerAsync(); + var userId = currentUser.Id; + + using (var stream = file.OpenReadStream()) + { + var fileEntity = await _fileStorageService.SaveFileAsync( + fileStream: stream, + fileName: file.FileName, + userId: userId, + featureName: featureName, + entityType: entityType, + entityId: entityId, + rawText: rawText + ); + + return Json(new + { + success = true, + message = "File uploaded successfully", + file = new + { + id = fileEntity.Id, + fileName = fileEntity.FileName, + fileExtension = fileEntity.FileExtension, + created = fileEntity.Created, + hasRawText = !string.IsNullOrEmpty(fileEntity.RawText) + } + }); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error uploading file: {ex}"); + return Json(new + { + success = false, + message = $"Error uploading file: {ex.Message}" + }); + } + } + + /// + /// Upload multiple files at once + /// + [HttpPost] + public async Task UploadMultipleFiles( + List files, + string featureName, + string entityType, + int entityId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + if (files == null || files.Count == 0) + return Json(new { success = false, message = "No files uploaded" }); + + try + { + var currentUser = await _workContext.GetCurrentCustomerAsync(); + var userId = currentUser.Id; + var uploadedFiles = new List(); + var errors = new List(); + + foreach (var file in files) + { + try + { + if (file.Length > 0) + { + using (var stream = file.OpenReadStream()) + { + var fileEntity = await _fileStorageService.SaveFileAsync( + fileStream: stream, + fileName: file.FileName, + userId: userId, + featureName: featureName, + entityType: entityType, + entityId: entityId + ); + + uploadedFiles.Add(new + { + id = fileEntity.Id, + fileName = fileEntity.FileName + fileEntity.FileExtension + }); + } + } + } + catch (Exception ex) + { + errors.Add($"{file.FileName}: {ex.Message}"); + } + } + + return Json(new + { + success = uploadedFiles.Count > 0, + message = $"Uploaded {uploadedFiles.Count} of {files.Count} files", + files = uploadedFiles, + errors = errors + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error uploading multiple files: {ex}"); + return Json(new + { + success = false, + message = $"Error: {ex.Message}" + }); + } + } + + #endregion + + #region Download Files + + /// + /// Download a file by ID + /// + [HttpGet] + public async Task DownloadFile( + int fileId, + string featureName, + string entityType, + int entityId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Unauthorized(); + + try + { + var currentUser = await _workContext.GetCurrentCustomerAsync(); + var userId = currentUser.Id; + + var (fileStream, fileInfo) = await _fileStorageService.GetFileByIdAsync( + fileId: fileId, + userId: userId, + featureName: featureName, + entityType: entityType, + entityId: entityId + ); + + var fileName = $"{fileInfo.FileName}{fileInfo.FileExtension}"; + var contentType = GetContentType(fileInfo.FileExtension); + + return File(fileStream, contentType, fileName); + } + catch (FileNotFoundException ex) + { + return NotFound(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error downloading file: {ex}"); + return BadRequest(new { success = false, message = ex.Message }); + } + } + + /// + /// Preview a file inline (for PDFs, images) + /// + [HttpGet] + public async Task PreviewFile( + int fileId, + string featureName, + string entityType, + int entityId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Unauthorized(); + + try + { + var currentUser = await _workContext.GetCurrentCustomerAsync(); + var userId = currentUser.Id; + + var (fileStream, fileInfo) = await _fileStorageService.GetFileByIdAsync( + fileId: fileId, + userId: userId, + featureName: featureName, + entityType: entityType, + entityId: entityId + ); + + var contentType = GetContentType(fileInfo.FileExtension); + + // Return inline for preview + return File(fileStream, contentType); + } + catch (FileNotFoundException ex) + { + return NotFound(new { success = false, message = ex.Message }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error previewing file: {ex}"); + return BadRequest(new { success = false, message = ex.Message }); + } + } + + #endregion + + #region List & Search Files + + /// + /// Get all files + /// + [HttpGet] + public async Task GetAllFiles() + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + try + { + var files = await _fileStorageService.GetAllFilesAsync(); + + return Json(new + { + success = true, + count = files.Count, + files = files.Select(f => new + { + id = f.Id, + fileName = f.FileName, + fileExtension = f.FileExtension, + fullName = $"{f.FileName}{f.FileExtension}", + created = f.Created, + modified = f.Modified, + hasRawText = !string.IsNullOrEmpty(f.RawText) + }) + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error getting files: {ex}"); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// Search files by filename or content + /// + [HttpGet] + public async Task SearchFiles(string searchTerm) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + if (string.IsNullOrWhiteSpace(searchTerm)) + return Json(new { success = false, message = "Search term is required" }); + + try + { + var files = await _fileStorageService.SearchFilesAsync(searchTerm); + + return Json(new + { + success = true, + searchTerm = searchTerm, + count = files.Count, + files = files.Select(f => new + { + id = f.Id, + fileName = f.FileName, + fileExtension = f.FileExtension, + fullName = $"{f.FileName}{f.FileExtension}", + created = f.Created, + hasRawText = !string.IsNullOrEmpty(f.RawText), + // Include snippet of matched text if available + textSnippet = GetTextSnippet(f.RawText, searchTerm) + }) + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error searching files: {ex}"); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// Get files for a specific entity + /// + [HttpGet] + public async Task GetEntityFiles(string entityType, int entityId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + try + { + // Example for ShippingDocuments - adapt for other entity types + if (entityType == "ShippingDocuments") + { + var mappings = await _dbContext.ShippingDocumentToFiles + .GetAll() + .Where(m => m.ShippingDocumentId == entityId) + .ToListAsync(); + + var fileIds = mappings.Select(m => m.FilesId).ToList(); + var files = await _dbContext.Files + .GetAll() + .Where(f => fileIds.Contains(f.Id)) + .ToListAsync(); + + return Json(new + { + success = true, + entityType = entityType, + entityId = entityId, + count = files.Count, + files = files.Select(f => new + { + id = f.Id, + fileName = $"{f.FileName}{f.FileExtension}", + created = f.Created, + documentType = mappings.FirstOrDefault(m => m.FilesId == f.Id)?.DocumentType.ToString() + }) + }); + } + + return Json(new + { + success = false, + message = $"Entity type '{entityType}' not supported yet" + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error getting entity files: {ex}"); + return Json(new { success = false, message = ex.Message }); + } + } + + #endregion + + #region Delete Files + + /// + /// Delete a file by ID + /// + [HttpPost] + public async Task DeleteFile( + int fileId, + string featureName, + string entityType, + int entityId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + try + { + var currentUser = await _workContext.GetCurrentCustomerAsync(); + var userId = currentUser.Id; + + var deleted = await _fileStorageService.DeleteFileAsync( + fileId: fileId, + userId: userId, + featureName: featureName, + entityType: entityType, + entityId: entityId + ); + + if (deleted) + { + // Also delete mappings (example for ShippingDocuments) + if (entityType == "ShippingDocuments") + { + var mappings = await _dbContext.ShippingDocumentToFiles + .GetAll() + .Where(m => m.FilesId == fileId && m.ShippingDocumentId == entityId) + .ToListAsync(); + + foreach (var mapping in mappings) + { + await _dbContext.ShippingDocumentToFiles.DeleteAsync(mapping); + } + + + } + } + + return Json(new + { + success = deleted, + message = deleted ? "File deleted successfully" : "File not found" + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error deleting file: {ex}"); + return Json(new + { + success = false, + message = $"Error deleting file: {ex.Message}" + }); + } + } + + /// + /// Delete multiple files at once + /// + [HttpPost] + public async Task DeleteMultipleFiles( + List fileIds, + string featureName, + string entityType, + int entityId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + if (fileIds == null || fileIds.Count == 0) + return Json(new { success = false, message = "No file IDs provided" }); + + try + { + var currentUser = await _workContext.GetCurrentCustomerAsync(); + var userId = currentUser.Id; + + var deletedCount = 0; + var errors = new List(); + + foreach (var fileId in fileIds) + { + try + { + var deleted = await _fileStorageService.DeleteFileAsync( + fileId: fileId, + userId: userId, + featureName: featureName, + entityType: entityType, + entityId: entityId + ); + + if (deleted) + { + deletedCount++; + } + } + catch (Exception ex) + { + errors.Add($"File {fileId}: {ex.Message}"); + } + } + + return Json(new + { + success = deletedCount > 0, + message = $"Deleted {deletedCount} of {fileIds.Count} files", + deletedCount = deletedCount, + errors = errors + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error deleting multiple files: {ex}"); + return Json(new + { + success = false, + message = $"Error: {ex.Message}" + }); + } + } + + #endregion + + #region File Information + + /// + /// Get file information by ID + /// + [HttpGet] + public async Task GetFileInfo(int fileId) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + try + { + var fileEntity = await _dbContext.Files.GetByIdAsync(fileId); + + if (fileEntity == null) + return NotFound(new { success = false, message = "File not found" }); + + return Json(new + { + success = true, + file = new + { + id = fileEntity.Id, + fileName = fileEntity.FileName, + fileExtension = fileEntity.FileExtension, + fullName = $"{fileEntity.FileName}{fileEntity.FileExtension}", + created = fileEntity.Created, + modified = fileEntity.Modified, + hasRawText = !string.IsNullOrEmpty(fileEntity.RawText), + rawTextLength = fileEntity.RawText?.Length ?? 0 + } + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error getting file info: {ex}"); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// Update file metadata (RawText) + /// + [HttpPost] + public async Task UpdateFileMetadata(int fileId, string rawText) + { + if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) + return Json(new { success = false, message = "Access denied" }); + + try + { + var fileEntity = await _dbContext.Files.GetByIdAsync(fileId); + + if (fileEntity == null) + return NotFound(new { success = false, message = "File not found" }); + + fileEntity.RawText = rawText; + await _fileStorageService.AddOrUpdateFileAsync(fileEntity); + + return Json(new + { + success = true, + message = "File metadata updated successfully" + }); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error updating file metadata: {ex}"); + return Json(new + { + success = false, + message = $"Error updating metadata: {ex.Message}" + }); + } + } + + #endregion + + #region Helper Methods + + /// + /// Get content type based on file extension + /// + private string GetContentType(string extension) + { + return extension.ToLowerInvariant() switch + { + ".pdf" => "application/pdf", + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".bmp" => "image/bmp", + ".txt" => "text/plain", + ".csv" => "text/csv", + ".json" => "application/json", + ".xml" => "application/xml", + ".html" => "text/html", + ".doc" => "application/msword", + ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".xls" => "application/vnd.ms-excel", + ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".zip" => "application/zip", + ".rar" => "application/x-rar-compressed", + _ => "application/octet-stream" + }; + } + + /// + /// Get a snippet of text around the search term + /// + private string GetTextSnippet(string text, string searchTerm, int contextLength = 100) + { + if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(searchTerm)) + return null; + + var index = text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase); + if (index == -1) + return null; + + var start = Math.Max(0, index - contextLength); + var length = Math.Min(text.Length - start, contextLength * 2 + searchTerm.Length); + + var snippet = text.Substring(start, length); + + if (start > 0) + snippet = "..." + snippet; + + if (start + length < text.Length) + snippet = snippet + "..."; + + return snippet; + } + + #endregion + } +} \ No newline at end of file diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml index 1486f46..96490f2 100644 --- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml +++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml @@ -1,5 +1,5 @@ @{ - Layout = "_ConfigurePlugin"; + Layout = "../_FruitBankEmptyAdminLayout.cshtml"; } @await Component.InvokeAsync("StoreScopeConfiguration") @@ -60,12 +60,12 @@ - +