Compare commits
2 Commits
02f7c768cb
...
2cd76a58a5
| Author | SHA1 | Date |
|---|---|---|
|
|
2cd76a58a5 | |
|
|
2c1e692e9d |
|
|
@ -226,7 +226,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
PaymentStatusIds = paymentStatuses,
|
PaymentStatusIds = paymentStatuses,
|
||||||
ShippingStatusIds = shippingStatuses,
|
ShippingStatusIds = shippingStatuses,
|
||||||
AvailablePageSizes = "20,50,100,500",
|
AvailablePageSizes = "20,50,100,500",
|
||||||
|
SortColumnDirection = "desc",
|
||||||
});
|
});
|
||||||
|
|
||||||
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/List.cshtml", model);
|
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/List.cshtml", model);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,177 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Nop.Core.Domain.Catalog;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||||
|
using Nop.Services.Catalog;
|
||||||
|
using Nop.Services.Security;
|
||||||
|
using Nop.Web.Framework;
|
||||||
|
using Nop.Web.Framework.Controllers;
|
||||||
|
using Nop.Web.Framework.Mvc.Filters;
|
||||||
|
using PDFtoImage;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBank.Controllers
|
||||||
{
|
{
|
||||||
public class FileManagerController : Controller
|
[AuthorizeAdmin]
|
||||||
|
[Area(AreaNames.ADMIN)]
|
||||||
|
[AutoValidateAntiforgeryToken]
|
||||||
|
public class FileManagerController : BasePluginController
|
||||||
{
|
{
|
||||||
public IActionResult BindingToFileSystem()
|
private readonly IPermissionService _permissionService;
|
||||||
|
private readonly OpenAIApiService _aiApiService;
|
||||||
|
private readonly IProductService _productService;
|
||||||
|
private readonly FruitBankDbContext _dbContext;
|
||||||
|
private readonly PdfToImageService _pdfToImageService;
|
||||||
|
|
||||||
|
public FileManagerController(
|
||||||
|
IPermissionService permissionService,
|
||||||
|
OpenAIApiService aiApiService,
|
||||||
|
IProductService productService,
|
||||||
|
FruitBankDbContext fruitBankDbContext,
|
||||||
|
PdfToImageService pdfToImageService)
|
||||||
{
|
{
|
||||||
return View();
|
_permissionService = permissionService;
|
||||||
|
_aiApiService = aiApiService;
|
||||||
|
_productService = productService;
|
||||||
|
_dbContext = fruitBankDbContext;
|
||||||
|
_pdfToImageService = pdfToImageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display the image text extraction page
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IActionResult> ImageTextExtraction()
|
||||||
|
{
|
||||||
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||||
|
return AccessDeniedView();
|
||||||
|
|
||||||
|
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint to extract text from uploaded image
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> ExtractTextFromImage(IFormFile imageFile, string customPrompt = null)
|
||||||
|
{
|
||||||
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||||
|
return Json(new { success = false, message = "Access denied" });
|
||||||
|
|
||||||
|
if (imageFile == null || imageFile.Length == 0)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = "No file received" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file type - now including PDF
|
||||||
|
var extension = 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." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Define the uploads folder
|
||||||
|
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "ocr");
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!Directory.Exists(uploadsFolder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(uploadsFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
string processedFilePath;
|
||||||
|
string processedFileName;
|
||||||
|
|
||||||
|
// Handle PDF conversion
|
||||||
|
if (extension == ".pdf")
|
||||||
|
{
|
||||||
|
// Save the PDF temporarily
|
||||||
|
var tempPdfFileName = $"temp_pdf_{DateTime.Now:yyyyMMdd_HHmmss}.pdf";
|
||||||
|
var tempPdfPath = Path.Combine(uploadsFolder, tempPdfFileName);
|
||||||
|
|
||||||
|
using (var stream = new FileStream(tempPdfPath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await imageFile.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert PDF to JPG using our service
|
||||||
|
var convertedImages = await _pdfToImageService.ConvertPdfToJpgAsync(tempPdfPath, uploadsFolder);
|
||||||
|
|
||||||
|
if (convertedImages == null || convertedImages.Count == 0)
|
||||||
|
{
|
||||||
|
// Clean up temp PDF
|
||||||
|
if (System.IO.File.Exists(tempPdfPath))
|
||||||
|
System.IO.File.Delete(tempPdfPath);
|
||||||
|
|
||||||
|
return Json(new { success = false, message = "Failed to convert PDF or PDF is empty" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first page
|
||||||
|
processedFilePath = convertedImages[0];
|
||||||
|
processedFileName = Path.GetFileName(processedFilePath);
|
||||||
|
|
||||||
|
// Clean up temp PDF
|
||||||
|
if (System.IO.File.Exists(tempPdfPath))
|
||||||
|
System.IO.File.Delete(tempPdfPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Handle regular image files
|
||||||
|
processedFileName = $"ocr_image_{DateTime.Now:yyyyMMdd_HHmmss}{extension}";
|
||||||
|
processedFilePath = Path.Combine(uploadsFolder, processedFileName);
|
||||||
|
|
||||||
|
using (var stream = new FileStream(processedFilePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await imageFile.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract text from the processed image using OpenAI Vision API
|
||||||
|
string extractedText;
|
||||||
|
using (var imageStream = new FileStream(processedFilePath, FileMode.Open, FileAccess.Read))
|
||||||
|
{
|
||||||
|
extractedText = await _aiApiService.ExtractTextFromImageAsync(
|
||||||
|
imageStream,
|
||||||
|
processedFileName,
|
||||||
|
customPrompt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(extractedText))
|
||||||
|
{
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "Failed to extract text. The API may have returned an empty response."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = extension == ".pdf"
|
||||||
|
? "PDF converted and text extracted successfully"
|
||||||
|
: "Text extracted successfully",
|
||||||
|
extractedText = extractedText,
|
||||||
|
fileName = processedFileName,
|
||||||
|
filePath = processedFilePath,
|
||||||
|
fileSize = imageFile.Length,
|
||||||
|
wasConverted = extension == ".pdf"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error in ExtractTextFromImage: {ex}");
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = $"Error processing file: {ex.Message}"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||||
|
using Nop.Services.Security;
|
||||||
|
using Nop.Web.Framework;
|
||||||
|
using Nop.Web.Framework.Controllers;
|
||||||
|
using Nop.Web.Framework.Mvc.Filters;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBank.Controllers
|
||||||
|
{
|
||||||
|
[AuthorizeAdmin]
|
||||||
|
[Area(AreaNames.ADMIN)]
|
||||||
|
[AutoValidateAntiforgeryToken]
|
||||||
|
public class FruitBankAudioController : BasePluginController
|
||||||
|
{
|
||||||
|
private readonly IPermissionService _permissionService;
|
||||||
|
private readonly OpenAIApiService _aiApiService;
|
||||||
|
private readonly FruitBankDbContext _dbContext;
|
||||||
|
|
||||||
|
public FruitBankAudioController(
|
||||||
|
IPermissionService permissionService,
|
||||||
|
OpenAIApiService aiApiService,
|
||||||
|
FruitBankDbContext fruitBankDbContext)
|
||||||
|
{
|
||||||
|
_permissionService = permissionService;
|
||||||
|
_aiApiService = aiApiService;
|
||||||
|
_dbContext = fruitBankDbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display the voice recorder page
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IActionResult> VoiceRecorder()
|
||||||
|
{
|
||||||
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||||
|
return AccessDeniedView();
|
||||||
|
|
||||||
|
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Extras/VoiceRecorder.cshtml");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint to receive voice recording
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> ReceiveVoiceRecording(IFormFile audioFile)
|
||||||
|
{
|
||||||
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||||
|
return Json(new { success = false, message = "Access denied" });
|
||||||
|
|
||||||
|
if (audioFile == null || audioFile.Length == 0)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = "No audio file received" });
|
||||||
|
}
|
||||||
|
|
||||||
|
string productDataSummary = string.Empty;
|
||||||
|
string orderDataSummary = string.Empty;
|
||||||
|
|
||||||
|
var products = await _dbContext.Products.GetAll().ToListAsync();
|
||||||
|
foreach (var product in products)
|
||||||
|
{
|
||||||
|
//let's prepare basic product and stock information for AI analysis
|
||||||
|
if (product == null) continue;
|
||||||
|
|
||||||
|
var productName = product.Name;
|
||||||
|
var stockQuantity = product.StockQuantity;
|
||||||
|
|
||||||
|
productDataSummary += $"Product: {productName}, Stock Quantity: {stockQuantity}\n";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var orders = await _dbContext.OrderDtos.GetAll(true).ToListAsync();
|
||||||
|
|
||||||
|
foreach (var order in orders)
|
||||||
|
{
|
||||||
|
//let's prepare basic order information for AI analysis
|
||||||
|
if (order == null) continue;
|
||||||
|
var orderId = order.Id;
|
||||||
|
var customerName = order.Customer.Company;
|
||||||
|
var totalAmount = order.OrderTotal;
|
||||||
|
var dateofReceipt = order.DateOfReceipt;
|
||||||
|
var isMeasurable = order.IsMeasurable;
|
||||||
|
var itemsInOrder = order.OrderItemDtos.Count;
|
||||||
|
orderDataSummary += $"Order ID: {orderId}, Customer: {customerName}, Total Amount: {totalAmount}, Date of Receipt: {dateofReceipt}, Is Measurable: {isMeasurable}, Items in Order: {itemsInOrder}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Console.WriteLine("Product Data Summary: " + productDataSummary);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Generate a unique filename
|
||||||
|
var fileName = $"voice_recording_{DateTime.Now:yyyyMMdd_HHmmss}.webm";
|
||||||
|
|
||||||
|
// Define the path where you want to save the file
|
||||||
|
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "voice");
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!Directory.Exists(uploadsFolder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(uploadsFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||||
|
|
||||||
|
// Save the file locally
|
||||||
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await audioFile.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transcribe the audio using OpenAI Whisper API
|
||||||
|
string transcribedText;
|
||||||
|
using (var audioStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||||
|
{
|
||||||
|
transcribedText = await _aiApiService.TranscribeAudioAsync(audioStream, fileName, "en"); // or "hu" for Hungarian
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(transcribedText))
|
||||||
|
{
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "Failed to transcribe audio"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string SystemMessage = "You are an assistant that helps with analyzing data for Fruitbank, a fruit trading company based in Hungary. " +
|
||||||
|
"Provide insights and suggestions based on the provided data." +
|
||||||
|
$"Products information: {productDataSummary}" +
|
||||||
|
$"Orders information: {orderDataSummary}";
|
||||||
|
|
||||||
|
|
||||||
|
var AIResponse = await _aiApiService.GetSimpleResponseAsync(SystemMessage, $"Transcribed Text: {transcribedText}");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = $"Audio transcribed successfully",
|
||||||
|
transcription = AIResponse,
|
||||||
|
filePath = filePath,
|
||||||
|
fileSize = audioFile.Length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = $"Error processing audio: {ex.Message}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
@{
|
||||||
|
Layout = "_ConfigurePlugin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@await Component.InvokeAsync("StoreScopeConfiguration")
|
||||||
|
|
||||||
|
<form asp-action="ExtractTextFromImage" asp-controller="FileManager" method="post">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>Image & PDF Text Extraction (OCR)</h4>
|
||||||
|
<p>Upload an image or PDF to extract text using OpenAI Vision API.</p>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="custom-file">
|
||||||
|
<input type="file" class="custom-file-input" id="imageFile" accept="image/*,.pdf,application/pdf">
|
||||||
|
<label class="custom-file-label" for="imageFile">Choose image or PDF file...</label>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">Supported formats: JPG, PNG, GIF, WebP, PDF</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<label for="customPrompt">Custom Prompt (Optional)</label>
|
||||||
|
<input type="text" class="form-control" id="customPrompt" placeholder="Leave empty for default: 'Olvasd ki a szöveget és add vissza szépen strukturálva.'">
|
||||||
|
<small class="form-text text-muted">Customize how the AI should extract and format the text</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<button type="button" id="uploadButton" class="btn btn-primary" disabled>
|
||||||
|
<i class="fas fa-upload"></i> Extract Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div id="responseMessage" class="alert" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row" id="filePreviewSection" style="display:none;">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<h5>File Preview:</h5>
|
||||||
|
<div id="imagePreviewContainer" style="display:none;">
|
||||||
|
<img id="imagePreview" src="" alt="Preview" style="max-width: 100%; max-height: 400px; border: 1px solid #ddd; padding: 5px;">
|
||||||
|
</div>
|
||||||
|
<div id="pdfPreviewContainer" style="display:none;">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-file-pdf"></i> PDF file selected. The first page will be converted to image for text extraction.
|
||||||
|
</div>
|
||||||
|
<embed id="pdfPreview" type="application/pdf" style="width: 100%; height: 500px; border: 1px solid #ddd;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row" id="extractedTextSection" style="display:none;">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<h5>Extracted Text:</h5>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<pre id="extractedText" style="white-space: pre-wrap; word-wrap: break-word; max-height: 500px; overflow-y: auto;"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="copyButton" class="btn btn-secondary mt-2">
|
||||||
|
<i class="fas fa-copy"></i> Copy to Clipboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const imageFileInput = document.getElementById('imageFile');
|
||||||
|
const customPromptInput = document.getElementById('customPrompt');
|
||||||
|
const uploadButton = document.getElementById('uploadButton');
|
||||||
|
const responseMessage = document.getElementById('responseMessage');
|
||||||
|
const filePreviewSection = document.getElementById('filePreviewSection');
|
||||||
|
const imagePreviewContainer = document.getElementById('imagePreviewContainer');
|
||||||
|
const pdfPreviewContainer = document.getElementById('pdfPreviewContainer');
|
||||||
|
const imagePreview = document.getElementById('imagePreview');
|
||||||
|
const pdfPreview = document.getElementById('pdfPreview');
|
||||||
|
const extractedTextSection = document.getElementById('extractedTextSection');
|
||||||
|
const extractedText = document.getElementById('extractedText');
|
||||||
|
const copyButton = document.getElementById('copyButton');
|
||||||
|
const fileLabel = document.querySelector('.custom-file-label');
|
||||||
|
|
||||||
|
let selectedFile = null;
|
||||||
|
|
||||||
|
// Update file label and enable upload button when file is selected
|
||||||
|
imageFileInput.addEventListener('change', (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
selectedFile = file;
|
||||||
|
fileLabel.textContent = file.name;
|
||||||
|
uploadButton.disabled = false;
|
||||||
|
|
||||||
|
// Check file type and show appropriate preview
|
||||||
|
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
|
||||||
|
|
||||||
|
if (isPdf) {
|
||||||
|
// Show PDF preview
|
||||||
|
imagePreviewContainer.style.display = 'none';
|
||||||
|
pdfPreviewContainer.style.display = 'block';
|
||||||
|
|
||||||
|
const fileUrl = URL.createObjectURL(file);
|
||||||
|
pdfPreview.src = fileUrl;
|
||||||
|
|
||||||
|
filePreviewSection.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
// Show image preview
|
||||||
|
pdfPreviewContainer.style.display = 'none';
|
||||||
|
imagePreviewContainer.style.display = 'block';
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
imagePreview.src = e.target.result;
|
||||||
|
filePreviewSection.style.display = 'block';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedFile = null;
|
||||||
|
fileLabel.textContent = 'Choose image or PDF file...';
|
||||||
|
uploadButton.disabled = true;
|
||||||
|
filePreviewSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
extractedTextSection.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadButton.addEventListener('click', async () => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
showMessage('Please select a file first!', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('imageFile', selectedFile);
|
||||||
|
|
||||||
|
const customPrompt = customPromptInput.value.trim();
|
||||||
|
if (customPrompt) {
|
||||||
|
formData.append('customPrompt', customPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
uploadButton.disabled = true;
|
||||||
|
const isPdf = selectedFile.type === 'application/pdf' || selectedFile.name.toLowerCase().endsWith('.pdf');
|
||||||
|
uploadButton.innerHTML = isPdf
|
||||||
|
? '<i class="fas fa-spinner fa-spin"></i> Converting PDF & Extracting...'
|
||||||
|
: '<i class="fas fa-spinner fa-spin"></i> Extracting...';
|
||||||
|
|
||||||
|
// Get the antiforgery token
|
||||||
|
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
||||||
|
|
||||||
|
const response = await fetch('@Url.Action("ExtractTextFromImage", "FileManager")', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'RequestVerificationToken': token
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
const message = result.wasConverted
|
||||||
|
? 'PDF converted and text extracted successfully!'
|
||||||
|
: 'Text extracted successfully!';
|
||||||
|
showMessage(message, 'success');
|
||||||
|
|
||||||
|
// Display extracted text
|
||||||
|
if (result.extractedText) {
|
||||||
|
extractedText.textContent = result.extractedText;
|
||||||
|
extractedTextSection.style.display = 'block';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showMessage('Error: ' + (result.message || 'Failed to extract text'), 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting text:', error);
|
||||||
|
showMessage('Error: Failed to communicate with server', 'danger');
|
||||||
|
} finally {
|
||||||
|
uploadButton.disabled = false;
|
||||||
|
uploadButton.innerHTML = '<i class="fas fa-upload"></i> Extract Text';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
copyButton.addEventListener('click', () => {
|
||||||
|
const textToCopy = extractedText.textContent;
|
||||||
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||||
|
const originalText = copyButton.innerHTML;
|
||||||
|
copyButton.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||||
|
copyButton.classList.remove('btn-secondary');
|
||||||
|
copyButton.classList.add('btn-success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
copyButton.innerHTML = originalText;
|
||||||
|
copyButton.classList.remove('btn-success');
|
||||||
|
copyButton.classList.add('btn-secondary');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy text:', err);
|
||||||
|
showMessage('Failed to copy to clipboard', 'warning');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showMessage(message, type) {
|
||||||
|
responseMessage.textContent = message;
|
||||||
|
responseMessage.className = 'alert alert-' + type;
|
||||||
|
responseMessage.style.display = 'block';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
responseMessage.style.display = 'none';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
@{
|
||||||
|
Layout = "_ConfigurePlugin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@await Component.InvokeAsync("StoreScopeConfiguration")
|
||||||
|
|
||||||
|
<form asp-action="ReceiveVoiceRecording" asp-controller="FruitBankAdmin" method="post">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>Voice Recorder</h4>
|
||||||
|
<p>Click the button below to start recording your voice message.</p>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<button type="button" id="recordButton" class="btn btn-primary">
|
||||||
|
<i class="fas fa-microphone"></i> Start Recording
|
||||||
|
</button>
|
||||||
|
<button type="button" id="stopButton" class="btn btn-danger" style="display:none;">
|
||||||
|
<i class="fas fa-stop"></i> Stop Recording
|
||||||
|
</button>
|
||||||
|
<span id="recordingStatus" style="margin-left: 15px; font-weight: bold;"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row" id="audioPlaybackSection" style="display:none;">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<audio id="audioPlayback" controls style="width: 100%;"></audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row" id="sendSection" style="display:none;">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<button type="button" id="sendButton" class="btn btn-success">
|
||||||
|
<i class="fas fa-paper-plane"></i> Send to API
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div id="responseMessage" class="alert" style="display:none;"></div>
|
||||||
|
<div id="transcriptionResult" style="display:none; margin-top: 15px;">
|
||||||
|
<h5>Transcription:</h5>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<p id="transcriptionText" style="white-space: pre-wrap;"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let mediaRecorder;
|
||||||
|
let audioChunks = [];
|
||||||
|
let audioBlob;
|
||||||
|
|
||||||
|
const recordButton = document.getElementById('recordButton');
|
||||||
|
const stopButton = document.getElementById('stopButton');
|
||||||
|
const sendButton = document.getElementById('sendButton');
|
||||||
|
const recordingStatus = document.getElementById('recordingStatus');
|
||||||
|
const audioPlayback = document.getElementById('audioPlayback');
|
||||||
|
const audioPlaybackSection = document.getElementById('audioPlaybackSection');
|
||||||
|
const sendSection = document.getElementById('sendSection');
|
||||||
|
const responseMessage = document.getElementById('responseMessage');
|
||||||
|
const transcriptionResult = document.getElementById('transcriptionResult');
|
||||||
|
const transcriptionText = document.getElementById('transcriptionText');
|
||||||
|
|
||||||
|
recordButton.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
mediaRecorder = new MediaRecorder(stream);
|
||||||
|
audioChunks = [];
|
||||||
|
|
||||||
|
mediaRecorder.ondataavailable = (event) => {
|
||||||
|
audioChunks.push(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.onstop = () => {
|
||||||
|
audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||||
|
const audioUrl = URL.createObjectURL(audioBlob);
|
||||||
|
audioPlayback.src = audioUrl;
|
||||||
|
audioPlaybackSection.style.display = 'block';
|
||||||
|
sendSection.style.display = 'block';
|
||||||
|
|
||||||
|
// Stop all tracks to release the microphone
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.start();
|
||||||
|
recordButton.style.display = 'none';
|
||||||
|
stopButton.style.display = 'inline-block';
|
||||||
|
recordingStatus.textContent = 'Recording...';
|
||||||
|
recordingStatus.style.color = 'red';
|
||||||
|
audioPlaybackSection.style.display = 'none';
|
||||||
|
sendSection.style.display = 'none';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error accessing microphone:', error);
|
||||||
|
showMessage('Error: Could not access microphone. Please check permissions.', 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopButton.addEventListener('click', () => {
|
||||||
|
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
recordButton.style.display = 'inline-block';
|
||||||
|
stopButton.style.display = 'none';
|
||||||
|
recordingStatus.textContent = 'Recording stopped';
|
||||||
|
recordingStatus.style.color = 'green';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sendButton.addEventListener('click', async () => {
|
||||||
|
if (!audioBlob) {
|
||||||
|
showMessage('No audio recorded yet!', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audioFile', audioBlob, 'recording.webm');
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendButton.disabled = true;
|
||||||
|
sendButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
||||||
|
|
||||||
|
// Get the antiforgery token
|
||||||
|
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
||||||
|
|
||||||
|
const response = await fetch('@Url.Action("ReceiveVoiceRecording", "FruitBankAudio")', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'RequestVerificationToken': token
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
showMessage('Audio transcribed successfully!', 'success');
|
||||||
|
|
||||||
|
// Display transcription
|
||||||
|
if (result.transcription) {
|
||||||
|
transcriptionText.textContent = result.transcription;
|
||||||
|
transcriptionResult.style.display = 'block';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showMessage('Error: ' + (result.message || 'Failed to transcribe audio'), 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending audio:', error);
|
||||||
|
showMessage('Error: Failed to send audio to server', 'danger');
|
||||||
|
} finally {
|
||||||
|
sendButton.disabled = false;
|
||||||
|
sendButton.innerHTML = '<i class="fas fa-paper-plane"></i> Send to API';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function showMessage(message, type) {
|
||||||
|
responseMessage.textContent = message;
|
||||||
|
responseMessage.className = 'alert alert-' + type;
|
||||||
|
responseMessage.style.display = 'block';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
responseMessage.style.display = 'none';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -400,10 +400,19 @@
|
||||||
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.InnvoiceTechId))
|
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.InnvoiceTechId))
|
||||||
{
|
{
|
||||||
Title = "Innvoiceba beküldve",
|
Title = "Innvoiceba beküldve",
|
||||||
Width = "150",
|
Width = "100",
|
||||||
Render = new RenderCustom("renderColumnInnvoiceTechId"),
|
Render = new RenderCustom("renderColumnInnvoiceTechId"),
|
||||||
ClassName = NopColumnClassDefaults.CenterAll
|
ClassName = NopColumnClassDefaults.CenterAll
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsAllOrderItemAvgWeightValid))
|
||||||
|
{
|
||||||
|
Title = "Súlyeltérés",
|
||||||
|
Width = "100",
|
||||||
|
Render = new RenderCustom("renderColumnAverageWeightError"),
|
||||||
|
ClassName = NopColumnClassDefaults.CenterAll
|
||||||
|
});
|
||||||
|
|
||||||
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable))
|
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable))
|
||||||
{
|
{
|
||||||
Title = T($"FruitBank.{nameof(OrderModelExtended.IsMeasurable)}").Text,
|
Title = T($"FruitBank.{nameof(OrderModelExtended.IsMeasurable)}").Text,
|
||||||
|
|
@ -542,7 +551,14 @@
|
||||||
if(data != null) {
|
if(data != null) {
|
||||||
return '<span class="badge badge-success" disabled>Igen</span>';
|
return '<span class="badge badge-success" disabled>Igen</span>';
|
||||||
}
|
}
|
||||||
return '<span class="badge badge-warning" disabled>Nem</span>';
|
return '<span class="badge badge-danger" disabled>Nem</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderColumnAverageWeightError(data, type, row, meta) {
|
||||||
|
if(data) {
|
||||||
|
return '<span class="badge badge-success" disabled>OK</span>';
|
||||||
|
}
|
||||||
|
return '<span class="badge badge-danger" disabled>!!!</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderColumnIsMeasurable(data, type, row, meta) {
|
function renderColumnIsMeasurable(data, type, row, meta) {
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,12 @@
|
||||||
<th>
|
<th>
|
||||||
Mérés állapota
|
Mérés állapota
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
Súlyeltérés
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Súlyeltérés mértéke
|
||||||
|
</th>
|
||||||
@* <th>
|
@* <th>
|
||||||
@T("Admin.Orders.Products.Discount")
|
@T("Admin.Orders.Products.Discount")
|
||||||
</th> *@
|
</th> *@
|
||||||
|
|
@ -336,6 +341,24 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td style="width: 80px;" class="text-center">
|
||||||
|
@if (!item.AverageWeightIsValid)
|
||||||
|
{
|
||||||
|
<span class="badge badge-danger" disabled>!!!</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge badge-success" disabled>OK</span>
|
||||||
|
}
|
||||||
|
<input type="hidden" name="pvAverageWeightIsValid@(item.Id)" id="pvAverageWeightIsValid@(item.Id)" value="@(item.AverageWeightIsValid.ToString())" disabled />
|
||||||
|
</td>
|
||||||
|
<td style="width: 80px;" class="text-center">
|
||||||
|
|
||||||
|
|
||||||
|
<span class="">Eltérés: @item.AverageWeightDifference (KG), Mért átlag: @item.AverageWeight (KG/rekesz)</span>
|
||||||
|
|
||||||
|
<input type="hidden" name="pvAverageWeightDifference@(item.Id)" id="pvAverageWeightDifference@(item.Id)" value="@(item.AverageWeightDifference.ToString())" disabled />
|
||||||
|
</td>
|
||||||
@* <td style="width: 15%;" class="text-center">
|
@* <td style="width: 15%;" class="text-center">
|
||||||
@if (Model.AllowCustomersToSelectTaxDisplayType)
|
@if (Model.AllowCustomersToSelectTaxDisplayType)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,10 @@
|
||||||
{
|
{
|
||||||
Title = "Súly(kg)"//T("Admin.Catalog.Products.Fields.NetWeight").Text
|
Title = "Súly(kg)"//T("Admin.Catalog.Products.Fields.NetWeight").Text
|
||||||
},
|
},
|
||||||
|
new ColumnProperty(nameof(ProductModelExtended.AverageWeight))
|
||||||
|
{
|
||||||
|
Title = "Átlagsúly (kg)"//T("Admin.Catalog.Products.Fields.AverageWeight").Text
|
||||||
|
},
|
||||||
new ColumnProperty(nameof(ProductModelExtended.Tare))
|
new ColumnProperty(nameof(ProductModelExtended.Tare))
|
||||||
{
|
{
|
||||||
Title = "Tára(kg)"//T("Admin.Catalog.Products.Fields.Tare").Text
|
Title = "Tára(kg)"//T("Admin.Catalog.Products.Fields.Tare").Text
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Components
|
||||||
|
|
||||||
model.Tare = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, double>(model.ProductId, nameof(ITare.Tare));
|
model.Tare = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, double>(model.ProductId, nameof(ITare.Tare));
|
||||||
model.IncomingQuantity = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, int>(model.ProductId, nameof(IIncomingQuantity.IncomingQuantity));
|
model.IncomingQuantity = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, int>(model.ProductId, nameof(IIncomingQuantity.IncomingQuantity));
|
||||||
|
model.AverageWeight = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, double>(model.ProductId, nameof(IProductDto.AverageWeight));
|
||||||
|
model.AverageWeightTreshold = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, double>(model.ProductId, nameof(IProductDto.AverageWeightTreshold));
|
||||||
}
|
}
|
||||||
|
|
||||||
return View("~/Plugins/Misc.FruitBankPlugin/Views/ProductAttributes.cshtml", model);
|
return View("~/Plugins/Misc.FruitBankPlugin/Views/ProductAttributes.cshtml", model);
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,20 @@ public class FruitBankEventConsumer :
|
||||||
if (productDto == null || productDto.Tare != tare)
|
if (productDto == null || productDto.Tare != tare)
|
||||||
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Product, double>(product.Id, nameof(ITare.Tare), tare);
|
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Product, double>(product.Id, nameof(ITare.Tare), tare);
|
||||||
|
|
||||||
|
//AverageWeight
|
||||||
|
var averageWeight = double.Round(CommonHelper.To<double>(form[nameof(IProductDto.AverageWeight)].ToString()), 1);
|
||||||
|
if (averageWeight < 0) throw new Exception($"FruitBankEventConsumer->SaveProductCustomAttributesAsync(); (averageWeight < 0); productId: {product.Id}; averageWeight: {averageWeight}");
|
||||||
|
|
||||||
|
if (productDto == null || productDto.AverageWeight != averageWeight)
|
||||||
|
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Product, double>(product.Id, nameof(IProductDto.AverageWeight), averageWeight);
|
||||||
|
|
||||||
|
//AverageWeightTreshold
|
||||||
|
var averageWeightTreshold = double.Round(CommonHelper.To<double>(form[nameof(IProductDto.AverageWeightTreshold)].ToString()), 1);
|
||||||
|
if (averageWeightTreshold < 0) throw new Exception($"FruitBankEventConsumer->SaveProductCustomAttributesAsync(); (averageWeightTreshold < 0); productId: {product.Id}; averageWeight: {averageWeightTreshold}");
|
||||||
|
|
||||||
|
if (productDto == null || productDto.AverageWeightTreshold != averageWeightTreshold)
|
||||||
|
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Product, double>(product.Id, nameof(IProductDto.AverageWeightTreshold), averageWeightTreshold);
|
||||||
|
|
||||||
//IncomingQuantity
|
//IncomingQuantity
|
||||||
var incomingQuantity = CommonHelper.To<int>(form[nameof(IIncomingQuantity.IncomingQuantity)].ToString());
|
var incomingQuantity = CommonHelper.To<int>(form[nameof(IIncomingQuantity.IncomingQuantity)].ToString());
|
||||||
if (incomingQuantity < 0) throw new Exception($"FruitBankEventConsumer->SaveProductCustomAttributesAsync(); (incomingQuantity < 0); productId: {product.Id}; incomingQuantity: {incomingQuantity}");
|
if (incomingQuantity < 0) throw new Exception($"FruitBankEventConsumer->SaveProductCustomAttributesAsync(); (incomingQuantity < 0); productId: {product.Id}; incomingQuantity: {incomingQuantity}");
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
||||||
orderModelExtended.IsMeasurable = orderDto.IsMeasurable;
|
orderModelExtended.IsMeasurable = orderDto.IsMeasurable;
|
||||||
orderModelExtended.DateOfReceipt = orderDto.DateOfReceipt;
|
orderModelExtended.DateOfReceipt = orderDto.DateOfReceipt;
|
||||||
orderModelExtended.OrderTotal = !orderDto.IsComplete && orderDto.IsMeasurable ? "kalkuláció alatt..." : orderModelExtended.OrderTotal;
|
orderModelExtended.OrderTotal = !orderDto.IsComplete && orderDto.IsMeasurable ? "kalkuláció alatt..." : orderModelExtended.OrderTotal;
|
||||||
|
orderModelExtended.IsAllOrderItemAvgWeightValid = orderDto.IsAllOrderItemAvgWeightValid;
|
||||||
|
|
||||||
|
|
||||||
//var fullName = $"{orderDto.Customer.FirstName}_{orderDto.Customer.LastName}".Trim();
|
//var fullName = $"{orderDto.Customer.FirstName}_{orderDto.Customer.LastName}".Trim();
|
||||||
orderModelExtended.CustomerCompany = $"{orderDto.Customer.Company} {orderDto.Customer.FirstName}_{orderDto.Customer.LastName}";
|
orderModelExtended.CustomerCompany = $"{orderDto.Customer.Company} {orderDto.Customer.FirstName}_{orderDto.Customer.LastName}";
|
||||||
|
|
@ -226,7 +228,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
||||||
orderItemModelExtended.ProductStockQuantity = orderItemDto.ProductDto!.StockQuantity;
|
orderItemModelExtended.ProductStockQuantity = orderItemDto.ProductDto!.StockQuantity;
|
||||||
orderItemModelExtended.ProductIncomingQuantity = orderItemDto.ProductDto.IncomingQuantity;
|
orderItemModelExtended.ProductIncomingQuantity = orderItemDto.ProductDto.IncomingQuantity;
|
||||||
orderItemModelExtended.ProductAvailableQuantity = orderItemDto.ProductDto.AvailableQuantity;
|
orderItemModelExtended.ProductAvailableQuantity = orderItemDto.ProductDto.AvailableQuantity;
|
||||||
|
orderItemModelExtended.AverageWeight = orderItemDto.AverageWeight;
|
||||||
|
orderItemModelExtended.AverageWeightIsValid = orderItemDto.AverageWeightIsValid;
|
||||||
|
orderItemModelExtended.AverageWeightDifference = orderItemDto.AverageWeightDifference;
|
||||||
|
|
||||||
orderItemModelExtended.SubTotalInclTax = orderItemDto.IsMeasurable && !orderItemDto.IsAudited ? "kalkuláció alatt..." : orderItemModelExtended.SubTotalInclTax;
|
orderItemModelExtended.SubTotalInclTax = orderItemDto.IsMeasurable && !orderItemDto.IsAudited ? "kalkuláció alatt..." : orderItemModelExtended.SubTotalInclTax;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ public class CustomProductModelFactory : MgProductModelFactory<ProductListModelE
|
||||||
productModelExtended.AvailableQuantity = productDto.AvailableQuantity;
|
productModelExtended.AvailableQuantity = productDto.AvailableQuantity;
|
||||||
|
|
||||||
productModelExtended.StockQuantityStr = productModelExtended.StockQuantity.ToString();
|
productModelExtended.StockQuantityStr = productModelExtended.StockQuantity.ToString();
|
||||||
|
|
||||||
|
productModelExtended.AverageWeight = productDto.AverageWeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
return productListModelExtended;
|
return productListModelExtended;
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,8 @@ public class PluginNopStartup : INopStartup
|
||||||
services.AddScoped<OpenAIApiService>();
|
services.AddScoped<OpenAIApiService>();
|
||||||
//services.AddScoped<IAIAPIService, OpenAIApiService>();
|
//services.AddScoped<IAIAPIService, OpenAIApiService>();
|
||||||
services.AddScoped<AICalculationService>();
|
services.AddScoped<AICalculationService>();
|
||||||
|
services.AddScoped<PdfToImageService>();
|
||||||
|
|
||||||
services.AddControllersWithViews(options =>
|
services.AddControllersWithViews(options =>
|
||||||
{
|
{
|
||||||
options.Filters.AddService<PendingMeasurementCheckoutFilter>();
|
options.Filters.AddService<PendingMeasurementCheckoutFilter>();
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,16 @@ public class RouteProvider : IRouteProvider
|
||||||
name: "Plugin.FruitBank.AppDownload.Download",
|
name: "Plugin.FruitBank.AppDownload.Download",
|
||||||
pattern: "Admin/AppDownload/Download/{version}/{fileName}",
|
pattern: "Admin/AppDownload/Download/{version}/{fileName}",
|
||||||
defaults: new { controller = "AppDownload", action = "Download", area = AreaNames.ADMIN });
|
defaults: new { controller = "AppDownload", action = "Download", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
|
name: "Plugin.FruitBank.Admin.FruitBankAudio",
|
||||||
|
pattern: "Admin/VoiceRecorder",
|
||||||
|
defaults: new { controller = "FruitBankAudio", action = "VoiceRecorder", area = AreaNames.ADMIN });
|
||||||
|
|
||||||
|
endpointRouteBuilder.MapControllerRoute(
|
||||||
|
name: "Plugin.FruitBank.Admin.ExtractText",
|
||||||
|
pattern: "Admin/ExtractText",
|
||||||
|
defaults: new { controller = "FileManager", action = "ImageTextExtraction", area = AreaNames.ADMIN });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Orders
|
||||||
public int ProductStockQuantity { get; set; }
|
public int ProductStockQuantity { get; set; }
|
||||||
public int ProductIncomingQuantity { get; set; }
|
public int ProductIncomingQuantity { get; set; }
|
||||||
public int ProductAvailableQuantity { get; set; }
|
public int ProductAvailableQuantity { get; set; }
|
||||||
|
public double AverageWeight { get; set; }
|
||||||
|
public bool AverageWeightIsValid { get; set; }
|
||||||
|
public double AverageWeightDifference { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial record OrderModelExtended : MgOrderModelExtended, IOrderModelExtended
|
public partial record OrderModelExtended : MgOrderModelExtended, IOrderModelExtended
|
||||||
|
|
@ -28,6 +34,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Orders
|
||||||
|
|
||||||
public string InnvoiceTechId { get; set; }
|
public string InnvoiceTechId { get; set; }
|
||||||
|
|
||||||
|
public bool IsAllOrderItemAvgWeightValid { get; set; }
|
||||||
|
|
||||||
public IList<OrderItemModelExtended> ItemExtendeds { get; set; }
|
public IList<OrderItemModelExtended> ItemExtendeds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models
|
||||||
|
|
||||||
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.Tare")]
|
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.Tare")]
|
||||||
public double Tare { get; set; }
|
public double Tare { get; set; }
|
||||||
|
|
||||||
|
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.AverageWeight")]
|
||||||
|
public double AverageWeight { get; set; }
|
||||||
|
|
||||||
|
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.AverageWeightTreshold")]
|
||||||
|
public double AverageWeightTreshold { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Products
|
||||||
public int IncomingQuantity { get; set; }
|
public int IncomingQuantity { get; set; }
|
||||||
|
|
||||||
public int AvailableQuantity { get; set; }
|
public int AvailableQuantity { get; set; }
|
||||||
|
|
||||||
|
public double AverageWeight { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="9.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||||
|
|
||||||
<!--<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />-->
|
<!--<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />-->
|
||||||
<PackageReference Include="PdfPig" Version="0.1.11" />
|
<PackageReference Include="PdfPig" Version="0.1.11" />
|
||||||
<PackageReference Include="PdfPig.Rendering.Skia" Version="0.1.11.5" />
|
<PackageReference Include="PdfPig.Rendering.Skia" Version="0.1.11.5" />
|
||||||
|
|
@ -35,6 +36,10 @@
|
||||||
<PackageReference Include="System.Memory.Data" Version="9.0.10" />
|
<PackageReference Include="System.Memory.Data" Version="9.0.10" />
|
||||||
<PackageReference Include="Tesseract" Version="5.2.0" />
|
<PackageReference Include="Tesseract" Version="5.2.0" />
|
||||||
<PackageReference Include="TesseractOCR" Version="5.5.1" />
|
<PackageReference Include="TesseractOCR" Version="5.5.1" />
|
||||||
|
<PackageReference Include="PDFtoImage" Version="4.0.2" />
|
||||||
|
<PackageReference Include="PDFtoImage" Version="4.0.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -171,6 +176,12 @@
|
||||||
<None Update="Areas\Admin\Views\AppDownload\Index.cshtml">
|
<None Update="Areas\Admin\Views\AppDownload\Index.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Areas\Admin\Views\Extras\ImageTextExtraction.cshtml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Areas\Admin\Views\Extras\VoiceRecorder.cshtml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
|
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|
@ -655,4 +666,10 @@
|
||||||
<Target Name="NopTarget" AfterTargets="Build">
|
<Target Name="NopTarget" AfterTargets="Build">
|
||||||
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
|
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
|
||||||
</Target>
|
</Target>
|
||||||
|
<Target Name="CopyPdfiumDll" AfterTargets="Build">
|
||||||
|
<ItemGroup>
|
||||||
|
<PdfiumFiles Include="$(NuGetPackageRoot)pdfium.binaries\**\pdfium.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Copy SourceFiles="@(PdfiumFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -20,6 +20,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
|
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||||
private const string OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations";
|
private const string OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations";
|
||||||
private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files";
|
private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files";
|
||||||
|
private const string OpenAiAudioEndpoint = "https://api.openai.com/v1/audio/transcriptions";
|
||||||
private const string BaseUrl = "https://api.openai.com/v1";
|
private const string BaseUrl = "https://api.openai.com/v1";
|
||||||
|
|
||||||
private string? _assistantId;
|
private string? _assistantId;
|
||||||
|
|
@ -43,6 +44,79 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
_onError = onErrorCallback;
|
_onError = onErrorCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region === AUDIO TRANSCRIPTION ===
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transcribe audio file to text using OpenAI Whisper API
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audioStream">The audio file stream</param>
|
||||||
|
/// <param name="fileName">The original filename (used to determine format)</param>
|
||||||
|
/// <param name="language">Optional language code (e.g., "en", "hu"). If null, auto-detects.</param>
|
||||||
|
/// <returns>The transcribed text</returns>
|
||||||
|
public async Task<string?> TranscribeAudioAsync(Stream audioStream, string fileName, string? language = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var form = new MultipartFormDataContent();
|
||||||
|
|
||||||
|
// Add the audio file
|
||||||
|
var audioContent = new StreamContent(audioStream);
|
||||||
|
|
||||||
|
// Determine content type based on file extension
|
||||||
|
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||||
|
audioContent.Headers.ContentType = extension switch
|
||||||
|
{
|
||||||
|
".mp3" => new MediaTypeHeaderValue("audio/mpeg"),
|
||||||
|
".mp4" => new MediaTypeHeaderValue("audio/mp4"),
|
||||||
|
".mpeg" => new MediaTypeHeaderValue("audio/mpeg"),
|
||||||
|
".mpga" => new MediaTypeHeaderValue("audio/mpeg"),
|
||||||
|
".m4a" => new MediaTypeHeaderValue("audio/m4a"),
|
||||||
|
".wav" => new MediaTypeHeaderValue("audio/wav"),
|
||||||
|
".webm" => new MediaTypeHeaderValue("audio/webm"),
|
||||||
|
_ => new MediaTypeHeaderValue("application/octet-stream")
|
||||||
|
};
|
||||||
|
|
||||||
|
form.Add(audioContent, "file", fileName);
|
||||||
|
|
||||||
|
// Add model
|
||||||
|
form.Add(new StringContent("whisper-1"), "model");
|
||||||
|
|
||||||
|
// Add language if specified
|
||||||
|
if (!string.IsNullOrEmpty(language))
|
||||||
|
{
|
||||||
|
form.Add(new StringContent(language), "language");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Add response format (json is default, can also be text, srt, verbose_json, or vtt)
|
||||||
|
form.Add(new StringContent("json"), "response_format");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync(OpenAiAudioEndpoint, form);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
Console.WriteLine($"Audio transcription failed: {error}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var contentStream = await response.Content.ReadAsStreamAsync();
|
||||||
|
using var json = await JsonDocument.ParseAsync(contentStream);
|
||||||
|
|
||||||
|
var transcription = json.RootElement.GetProperty("text").GetString();
|
||||||
|
|
||||||
|
Console.WriteLine($"Audio transcription successful. Length: {transcription?.Length ?? 0} characters");
|
||||||
|
|
||||||
|
return transcription;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error transcribing audio: {ex}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region === CHAT (TEXT INPUT) ===
|
#region === CHAT (TEXT INPUT) ===
|
||||||
|
|
||||||
public async Task<string?> GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null)
|
public async Task<string?> GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null)
|
||||||
|
|
@ -682,6 +756,99 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region === IMAGE TEXT EXTRACTION ===
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extract text from an image using OpenAI Vision API
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="imageStream">The image file stream</param>
|
||||||
|
/// <param name="fileName">The original filename</param>
|
||||||
|
/// <param name="customPrompt">Optional custom prompt for text extraction</param>
|
||||||
|
/// <returns>Extracted and structured text from the image</returns>
|
||||||
|
public async Task<string?> ExtractTextFromImageAsync(Stream imageStream, string fileName, string? customPrompt = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Read image bytes from stream
|
||||||
|
byte[] imgBytes;
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
await imageStream.CopyToAsync(memoryStream);
|
||||||
|
imgBytes = memoryStream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
string base64 = Convert.ToBase64String(imgBytes);
|
||||||
|
|
||||||
|
// Determine image format
|
||||||
|
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||||
|
var mimeType = extension switch
|
||||||
|
{
|
||||||
|
".jpg" or ".jpeg" => "image/jpeg",
|
||||||
|
".png" => "image/png",
|
||||||
|
".gif" => "image/gif",
|
||||||
|
".webp" => "image/webp",
|
||||||
|
_ => "image/jpeg"
|
||||||
|
};
|
||||||
|
|
||||||
|
var prompt = customPrompt ?? "Olvasd ki a szöveget és add vissza szépen strukturálva.";
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
model = GetModelName(), // Use the configured model
|
||||||
|
messages = new object[]
|
||||||
|
{
|
||||||
|
new {
|
||||||
|
role = "user",
|
||||||
|
content = new object[]
|
||||||
|
{
|
||||||
|
new { type = "text", text = prompt },
|
||||||
|
new { type = "image_url", image_url = new { url = $"data:{mimeType};base64,{base64}" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync(
|
||||||
|
OpenAiEndpoint,
|
||||||
|
new StringContent(json, Encoding.UTF8, "application/json"));
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
Console.WriteLine($"Image text extraction failed: {error}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultJson = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(resultJson);
|
||||||
|
|
||||||
|
var inputTokens = doc.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32();
|
||||||
|
var outputTokens = doc.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32();
|
||||||
|
Console.WriteLine($"USAGE STATS - Image OCR Tokens: {inputTokens} + {outputTokens} = {inputTokens + outputTokens}");
|
||||||
|
|
||||||
|
string text = doc.RootElement
|
||||||
|
.GetProperty("choices")[0]
|
||||||
|
.GetProperty("message")
|
||||||
|
.GetProperty("content")
|
||||||
|
.GetString();
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error extracting text from image: {ex}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using PDFtoImage;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
public class PdfToImageService
|
||||||
|
{
|
||||||
|
private readonly PdfiumBootstrapper _pdfiumBootstrapper;
|
||||||
|
|
||||||
|
public PdfToImageService(IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
_pdfiumBootstrapper = new PdfiumBootstrapper(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> ConvertPdfToJpgAsync(string pdfPath, string outputFolder)
|
||||||
|
{
|
||||||
|
_pdfiumBootstrapper.EnsurePdfiumLoaded();
|
||||||
|
|
||||||
|
var imagePaths = new List<string>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(outputFolder);
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
byte[] pdfBytes = File.ReadAllBytes(pdfPath);
|
||||||
|
|
||||||
|
var options = new RenderOptions
|
||||||
|
{
|
||||||
|
Dpi = 300,
|
||||||
|
BackgroundColor = SKColors.White
|
||||||
|
};
|
||||||
|
|
||||||
|
var pdfImages = PDFtoImage.Conversion.ToImages(pdfBytes, options: options);
|
||||||
|
|
||||||
|
int pageNumber = 1;
|
||||||
|
foreach (var page in pdfImages)
|
||||||
|
{
|
||||||
|
var outputPath = Path.Combine(outputFolder, $"page_{pageNumber}.jpg");
|
||||||
|
|
||||||
|
using (var fileStream = File.Create(outputPath))
|
||||||
|
{
|
||||||
|
page.Encode(fileStream, SKEncodedImageFormat.Jpeg, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePaths.Add(outputPath);
|
||||||
|
page.Dispose();
|
||||||
|
pageNumber++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return imagePaths;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Error converting PDF to images: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PdfiumBootstrapper
|
||||||
|
{
|
||||||
|
private readonly IWebHostEnvironment _env;
|
||||||
|
private bool _loaded = false;
|
||||||
|
|
||||||
|
public PdfiumBootstrapper(IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
_env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsurePdfiumLoaded()
|
||||||
|
{
|
||||||
|
if (_loaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pluginPath = Path.Combine(
|
||||||
|
_env.ContentRootPath,
|
||||||
|
"Plugins",
|
||||||
|
"Misc.FruitBankPlugin" // <- change this
|
||||||
|
);
|
||||||
|
|
||||||
|
var archFolder = Environment.Is64BitProcess ? "win-x64" : "win-x86";
|
||||||
|
var dllPath = Path.Combine(pluginPath, "runtimes", archFolder, "native", "pdfium.dll");
|
||||||
|
|
||||||
|
if (!File.Exists(dllPath))
|
||||||
|
throw new FileNotFoundException($"Pdfium.dll not found: {dllPath}");
|
||||||
|
|
||||||
|
NativeLibrary.Load(dllPath);
|
||||||
|
_loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<nop-label asp-for="NetWeight" />
|
<nop-label asp-for="NetWeight" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<nop-editor contenteditable="@Model.IsMeasurable" asp-for="NetWeight" />
|
<nop-editor readonly="@Model.IsMeasurable" asp-for="NetWeight" />
|
||||||
<span asp-validation-for="NetWeight"></span>
|
<span asp-validation-for="NetWeight"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,5 +46,24 @@
|
||||||
<span asp-validation-for="Tare"></span>
|
<span asp-validation-for="Tare"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<nop-label asp-for="AverageWeight" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<nop-editor asp-for="AverageWeight" />
|
||||||
|
<span asp-validation-for="AverageWeight"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<nop-label asp-for="AverageWeightTreshold" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<nop-editor asp-for="AverageWeightTreshold" />
|
||||||
|
<span asp-validation-for="AverageWeightTreshold"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
Loading…
Reference in New Issue