This commit is contained in:
Adam 2025-10-02 09:19:41 +02:00
parent 63abc01006
commit f787582604
16 changed files with 750 additions and 117 deletions

View File

@ -0,0 +1,25 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.ShippingDocumentListModel
@using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
@using FruitBank.Common.Entities;
@using DevExtreme.AspNet.Mvc
<h4>Id: @Model.ShippingId</h4>
@(Html.DevExtreme().DataGrid<ShippingDocument>()
.ID("documentsGrid")
.DataSource(Model.ShippingDocumentList)
.KeyExpr("Id")
.ShowBorders(true)
.Editing(editing => {
editing.Mode(GridEditMode.Row);
editing.AllowUpdating(true);
editing.AllowAdding(false);
editing.AllowDeleting(true);
})
.Columns(c => {
c.AddFor(m => m.DocumentDate).Caption("Date").DataType(GridDataType.Date);
c.AddFor(m => m.SenderName).Caption("Sender");
c.AddFor(m => m.InvoiceNumber).Caption("Invoice #");
c.AddFor(m => m.TotalAmount).Caption("Amount").DataType(GridDataType.Number);
c.AddFor(m => m.ItemCount).Caption("ItemCount").DataType(GridDataType.Number);
})
)

View File

@ -29,6 +29,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
{ {
ApiKey = _settings.ApiKey, ApiKey = _settings.ApiKey,
ModelName = _settings.ModelName, ModelName = _settings.ModelName,
OpenAIApiKey = _settings.OpenAIApiKey,
OpenAIModelName = _settings.OpenAIModelName,
IsEnabled = _settings.IsEnabled, IsEnabled = _settings.IsEnabled,
ApiBaseUrl = _settings.ApiBaseUrl, ApiBaseUrl = _settings.ApiBaseUrl,
MaxTokens = _settings.MaxTokens, MaxTokens = _settings.MaxTokens,
@ -49,8 +51,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
// Map model properties to settings // Map model properties to settings
_settings.ApiKey = model.ApiKey ?? string.Empty; _settings.ApiKey = model.ApiKey ?? string.Empty;
_settings.ModelName = model.ModelName ?? string.Empty; _settings.ModelName = model.ModelName ?? string.Empty;
_settings.OpenAIApiKey = model.OpenAIApiKey ?? string.Empty;
_settings.OpenAIModelName = model.OpenAIModelName ?? string.Empty;
_settings.IsEnabled = model.IsEnabled; _settings.IsEnabled = model.IsEnabled;
_settings.ApiBaseUrl = model.ApiBaseUrl ?? string.Empty; _settings.ApiBaseUrl = model.ApiBaseUrl ?? string.Empty;
_settings.OpenAIApiBaseUrl = model.OpenAIApiBaseUrl ?? string.Empty;
_settings.MaxTokens = model.MaxTokens; _settings.MaxTokens = model.MaxTokens;
_settings.Temperature = model.Temperature; _settings.Temperature = model.Temperature;
_settings.RequestTimeoutSeconds = model.RequestTimeoutSeconds; _settings.RequestTimeoutSeconds = model.RequestTimeoutSeconds;

View File

@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models; using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Plugin.Misc.FruitBankPlugin.Helpers;
using Nop.Plugin.Misc.FruitBankPlugin.Services;
using Nop.Services.Messages; using Nop.Services.Messages;
using Nop.Services.Security; using Nop.Services.Security;
using Nop.Web.Areas.Admin.Controllers; using Nop.Web.Areas.Admin.Controllers;
@ -11,6 +13,7 @@ using Nop.Web.Framework;
using Nop.Web.Framework.Models; using Nop.Web.Framework.Models;
using Nop.Web.Framework.Models.Extensions; using Nop.Web.Framework.Models.Extensions;
using Nop.Web.Framework.Mvc.Filters; using Nop.Web.Framework.Mvc.Filters;
using System.Text;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{ {
@ -24,12 +27,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
protected readonly ShippingDbTable _shippingDbTable; protected readonly ShippingDbTable _shippingDbTable;
protected readonly ShippingDocumentDbTable _shippingDocumentDbTable; protected readonly ShippingDocumentDbTable _shippingDocumentDbTable;
protected readonly FruitBankDbContext _dbContext; protected readonly FruitBankDbContext _dbContext;
//private readonly IFruitBankShippingModelFactory _shippingModelFactory; protected readonly AICalculationService _aiCalculationService;
// TODO: Add your shipment and document services here
// private readonly IShipmentService _shipmentService;
// private readonly IShipmentDocumentService _documentService;
public ShippingController(IPermissionService permissionService, INotificationService notificationService, ShippingItemDbTable shippingItemDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, FruitBankDbContext dbContext)
public ShippingController(IPermissionService permissionService, INotificationService notificationService, ShippingItemDbTable shippingItemDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, FruitBankDbContext dbContext, AICalculationService aICalculationService)
{ {
_permissionService = permissionService; _permissionService = permissionService;
_notificationService = notificationService; _notificationService = notificationService;
@ -38,6 +39,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
_shippingDocumentDbTable = shippingDocumentDbTable; _shippingDocumentDbTable = shippingDocumentDbTable;
_dbContext = dbContext; _dbContext = dbContext;
//_shippingModelFactory = shippingModelFactory; //_shippingModelFactory = shippingModelFactory;
_aiCalculationService = aICalculationService;
} }
[HttpGet] [HttpGet]
@ -246,26 +248,28 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> UploadFile(IFormFile file, int shipmentId) [RequestSizeLimit(10485760)] // 10MB
[RequestFormLimits(MultipartBodyLengthLimit = 10485760)]
public async Task<IActionResult> UploadFile(IFormFile file, int shippingId)
{ {
try try
{ {
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Json(new FileUploadResult { Success = false, ErrorMessage = "Access denied" }); return Json(new { success = false, errorMessage = "Access denied" });
if (file == null || file.Length == 0) if (file == null || file.Length == 0)
return Json(new FileUploadResult { Success = false, ErrorMessage = "No file selected" }); return Json(new { success = false, errorMessage = "No file selected" });
// Validate file type (PDF only) // Validate file type (PDF only)
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
return Json(new FileUploadResult { Success = false, ErrorMessage = "Only PDF files are allowed" }); return Json(new { success = false, errorMessage = "Only PDF files are allowed" });
// Validate file size (e.g., max 10MB) // Validate file size (max 10MB)
if (file.Length > 10 * 1024 * 1024) if (file.Length > 10 * 1024 * 1024)
return Json(new FileUploadResult { Success = false, ErrorMessage = "File size must be less than 10MB" }); return Json(new { success = false, errorMessage = "File size must be less than 10MB" });
// Create upload directory if it doesn't exist // Create upload directory if it doesn't exist
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shipments"); var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shippingDocuments");
Directory.CreateDirectory(uploadsPath); Directory.CreateDirectory(uploadsPath);
// Generate unique filename // Generate unique filename
@ -278,34 +282,115 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
await file.CopyToAsync(stream); await file.CopyToAsync(stream);
} }
// TODO: Save document record to database // Extract text with PdfPig
// var document = new ShipmentDocument var pdfText = new StringBuilder();
// { using (var pdf = UglyToad.PdfPig.PdfDocument.Open(filePath))
// ShipmentId = shipmentId, {
// FileName = file.FileName, foreach (var page in pdf.GetPages())
// FilePath = $"/uploads/shipments/{fileName}", {
// FileSize = (int)(file.Length / 1024), // Convert to KB pdfText.AppendLine(page.Text);
// ContentType = file.ContentType, }
// UploadDate = DateTime.UtcNow, }
// IsActive = true
// }; // Log extracted text for debugging
Console.WriteLine("Extracted PDF text:");
Console.WriteLine(pdfText.ToString());
// Analyze PDF with AI to extract structured data
var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalyzisFromText(
pdfText.ToString(),
"Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field."
);
// Parse AI response (assuming it returns JSON)
var extractedData = ParseShippingDocumentAIResponse(aiAnalysis);
//TODO: Save document record to database
Console.WriteLine("AI Analysis Result:");
Console.WriteLine(extractedData.RecipientName);
Console.WriteLine(extractedData.SenderName);
Console.WriteLine(extractedData.InvoiceNumber);
Console.WriteLine(extractedData.TotalAmount);
Console.WriteLine(extractedData.ItemCount);
Console.WriteLine(extractedData.Notes);
// var savedDocument = await _documentService.InsertDocumentAsync(document); // var savedDocument = await _documentService.InsertDocumentAsync(document);
return Json(new FileUploadResult // Mock saved document ID
var documentId = 1; // Replace with: savedDocument.Id
// Return structured document model
var documentModel = new ShippingDocumentModel
{ {
Success = true, Id = documentId,
ShippingId = shippingId,
FileName = file.FileName, FileName = file.FileName,
FilePath = $"/uploads/shippingDocuments/{fileName}", FilePath = $"/uploads/shippingDocuments/{fileName}",
FileSize = (int)(file.Length / 1024), // KB FileSize = (int)(file.Length / 1024),
DocumentId = 1 // Replace with: savedDocument.Id DocumentDate = extractedData.DocumentDate,
RecipientName = extractedData.RecipientName,
SenderName = extractedData.SenderName,
InvoiceNumber = extractedData.InvoiceNumber,
TotalAmount = extractedData.TotalAmount,
ItemCount = extractedData.ItemCount,
Notes = extractedData.Notes,
RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging
};
return Json(new
{
success = true,
document = documentModel
}); });
} }
catch (Exception ex) catch (Exception ex)
{ {
return Json(new FileUploadResult { Success = false, ErrorMessage = ex.Message }); Console.WriteLine($"Error uploading file: {ex}");
return Json(new { success = false, errorMessage = ex.Message });
} }
} }
//public IActionResult ReloadPartialView()
//{
// // ... (logic to get updated data for the partial view) ...
// var model = new ShippingDocumentListModel();
// return PartialView("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml", model);
//}
private ExtractedDocumentData ParseShippingDocumentAIResponse(string aiResponse)
{
try
{
// Try to parse as JSON first
var data = System.Text.Json.JsonSerializer.Deserialize<ExtractedDocumentData>(
aiResponse,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
return data ?? new ExtractedDocumentData();
}
catch
{
// If JSON parsing fails, return empty data with the raw response in notes
return new ExtractedDocumentData
{
Notes = $"AI Analysis (raw): {aiResponse}"
};
}
}
// Helper class for extracted data
private class ExtractedDocumentData
{
public DateTime? DocumentDate { get; set; }
public string RecipientName { get; set; }
public string SenderName { get; set; }
public string InvoiceNumber { get; set; }
public decimal? TotalAmount { get; set; }
public int? ItemCount { get; set; }
public string Notes { get; set; }
}
[HttpPost] [HttpPost]
public async Task<IActionResult> DeleteUploadedFile(string filePath) public async Task<IActionResult> DeleteUploadedFile(string filePath)
{ {

View File

@ -15,12 +15,21 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ModelName")] [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ModelName")]
public string ModelName { get; set; } = string.Empty; public string ModelName { get; set; } = string.Empty;
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiKey")]
public string OpenAIApiKey { get; set; } = string.Empty;
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ModelName")]
public string OpenAIModelName { get; set; } = string.Empty;
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.IsEnabled")] [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.IsEnabled")]
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiBaseUrl")] [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiBaseUrl")]
public string ApiBaseUrl { get; set; } = string.Empty; public string ApiBaseUrl { get; set; } = string.Empty;
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiBaseUrl")]
public string OpenAIApiBaseUrl { get; set; } = string.Empty;
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.MaxTokens")] [NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.MaxTokens")]
public int MaxTokens { get; set; } public int MaxTokens { get; set; }

View File

@ -27,17 +27,23 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
public class ShippingDocumentModel public class ShippingDocumentModel
{ {
public int Id { get; set; } public int Id { get; set; }
public ShippingDocument ShippingDocument { get; set; } public int ShippingId { get; set; }
public string FileName { get; set; } public string FileName { get; set; }
public string FilePath { get; set; } public string FilePath { get; set; }
public int FileSize { get; set; } // in KB public int FileSize { get; set; }
public DateTime UploadDate { get; set; }
public string ContentType { get; set; } // Extracted data from PDF
public bool IsActive { get; set; } = true; public DateTime? DocumentDate { get; set; }
public string RecipientName { get; set; }
public string SenderName { get; set; }
public string InvoiceNumber { get; set; }
public decimal? TotalAmount { get; set; }
public int? ItemCount { get; set; }
public string Notes { get; set; }
public string RawAIAnalysis { get; set; }
public ShippingDocument? ShippingDocument { get; set; }
// Computed properties for display
public string FormattedFileSize => $"{FileSize:N0} KB";
public string FormattedUploadDate => UploadDate.ToString("yyyy-MM-dd HH:mm");
} }
// Result model for AJAX operations // Result model for AJAX operations

View File

@ -0,0 +1,18 @@
using FruitBank.Common.Entities;
using Nop.Web.Framework.Models;
using Nop.Web.Framework.Mvc.ModelBinding;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
{
public record ShippingDocumentListModel : BaseNopModel
{
public ShippingDocumentListModel()
{
ShippingDocumentList = new List<ShippingDocumentModel>();
}
public int ShippingId { get; set; }
public List<ShippingDocumentModel> ShippingDocumentList { get; set; }
}
}

View File

@ -31,6 +31,19 @@
<span asp-validation-for="ModelName" class="text-danger"></span> <span asp-validation-for="ModelName" class="text-danger"></span>
<small class="form-text text-muted">Az AI modell neve (pl. gpt-3.5-turbo, gpt-4, claude-3-sonnet)</small> <small class="form-text text-muted">Az AI modell neve (pl. gpt-3.5-turbo, gpt-4, claude-3-sonnet)</small>
</div> </div>
<div class="form-group">
<label asp-for="OpenAIApiKey"></label>
<input asp-for="OpenAIApiKey" class="form-control" type="password" placeholder="Adja meg az OpenAI API kulcsot" />
<span asp-validation-for="OpenAIApiKey" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OpenAIModelName"></label>
<input asp-for="OpenAIModelName" class="form-control" placeholder="pl. gpt-3.5-turbo, gpt-4" />
<span asp-validation-for="OpenAIModelName" class="text-danger"></span>
<small class="form-text text-muted">Az AI modell neve (pl. gpt-3.5-turbo, gpt-4)</small>
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="ApiBaseUrl"></label> <label asp-for="ApiBaseUrl"></label>
@ -39,6 +52,13 @@
<small class="form-text text-muted">Az API alapcíme (OpenAI, Azure OpenAI, stb.)</small> <small class="form-text text-muted">Az API alapcíme (OpenAI, Azure OpenAI, stb.)</small>
</div> </div>
<div class="form-group">
<label asp-for="OpenAIApiBaseUrl"></label>
<input asp-for="OpenAIApiBaseUrl" class="form-control" placeholder="https://api.openai.com/v1" />
<span asp-validation-for="OpenAIApiBaseUrl" class="text-danger"></span>
<small class="form-text text-muted">Az API alapcíme (OpenAI, Azure OpenAI, stb.)</small>
</div>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group"> <div class="form-group">

View File

@ -55,7 +55,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -91,32 +91,12 @@
</div> </div>
<!-- Existing Documents --> <!-- Existing Documents -->
@if (Model.ExistingDocuments != null && Model.ExistingDocuments.Any()) <div class="mt-3">
{ <h5>Existing Documents:</h5>
<div class="mt-3"> <div id="documentsGridContainer">
<h5>Existing Documents:</h5> @await Html.PartialAsync("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Components/_DocumentsGridPartial.cshtml", Model.ExistingDocuments)
<div id="existingFilesList" class="list-group">
@foreach (var doc in Model.ExistingDocuments)
{
<div class="file-item">
<div class="file-info">
<i class="fas fa-file-pdf"></i>
<span>@doc.FileName</span>
<small class="text-muted">(@doc.FileSize KB)</small>
</div>
<div class="file-actions">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="viewFile('@doc.FilePath')">
<i class="fas fa-eye"></i> View
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteExistingFile('@doc.Id', this)">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
}
</div>
</div> </div>
} </div>
<!-- Newly Uploaded Files List --> <!-- Newly Uploaded Files List -->
<div id="uploadedFiles" class="mt-3" style="display: none;"> <div id="uploadedFiles" class="mt-3" style="display: none;">
@ -190,6 +170,14 @@
</style> </style>
<script> <script>
let grid;
function onGridInitialized(e) {
grid = e.component; // save instance
}
$(document).ready(function() { $(document).ready(function() {
let uploadedFiles = []; let uploadedFiles = [];
const ShippingId = @Model.Id; const ShippingId = @Model.Id;
@ -199,6 +187,12 @@
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const selectFilesBtn = document.getElementById('selectFilesBtn'); const selectFilesBtn = document.getElementById('selectFilesBtn');
function addDocumentToGrid(doc) {
grid.getDataSource().store().insert(doc).done(() => grid.refresh());
}
// Click to select files // Click to select files
selectFilesBtn.addEventListener('click', () => fileInput.click()); selectFilesBtn.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('click', (e) => { dropZone.addEventListener('click', (e) => {
@ -256,61 +250,92 @@
}); });
} }
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('ShippingId', ShippingId);
// Show upload progress function uploadFile(file) {
showUploadProgress(`Uploading ${file.name}...`); const formData = new FormData();
formData.append('file', file);
formData.append('ShippingId', ShippingId);
formData.append('__RequestVerificationToken', $('input[name="__RequestVerificationToken"]').val());
$.ajax({ showUploadProgress(`Uploading and processing ${file.name}...`);
url: '@Url.Action("UploadFile")',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(result) {
hideUploadProgress();
if (result.success) { $.ajax({
uploadedFiles.push(result); url: '@Url.Action("UploadFile", "Shipping")',
addFileToList(result); type: 'POST',
showSuccess(`"${result.fileName}" uploaded successfully`); data: formData,
} else { processData: false,
showError(`Upload failed: ${result.errorMessage}`); contentType: false,
} success: function(result) {
}, hideUploadProgress();
error: function(xhr, status, error) {
hideUploadProgress(); console.log('Upload result:', result); // Debug log
showError(`Upload failed: ${error}`);
if (result.success) {
showSuccess(`"${result.document.FileName}" uploaded and processed successfully`);
addDocumentToExistingList(result.document);
// Call a new endpoint to get the updated partial view
reloadPartialView();
} else {
showError(`Upload failed: ${result.errorMessage}`);
} }
}); },
} error: function(xhr, status, error) {
hideUploadProgress();
showError(`An error occurred during the upload: ${error}`);
}
});
}
function addFileToList(file) { // New function to reload the partial view
const filesList = document.getElementById('filesList'); function reloadPartialView() {
const fileItem = document.createElement('div'); // You'll need to create a new controller action for this endpoint
fileItem.className = 'file-item'; // that returns the partial view with the updated list of documents.
fileItem.innerHTML = ` $.ajax({
<div class="file-info"> url: '@Url.Action("ReloadPartialView", "Shipping")',
<i class="fas fa-file-pdf"></i> type: 'GET',
<span>${file.fileName}</span> success: function(html) {
</div> // Replace the content of the container holding the partial view
<div class="file-actions"> $('#documentsGridContainer').html(html);
<button type="button" class="btn btn-sm btn-outline-primary" onclick="viewFile('${file.filePath}')"> },
<i class="fas fa-eye"></i> View error: function(xhr, status, error) {
</button> console.error('Error reloading documents partial view:', error);
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFile('${file.filePath}', this)"> }
<i class="fas fa-trash"></i> Delete });
</button> }
</div>
`;
filesList.appendChild(fileItem);
// Show the uploaded files section
document.getElementById('uploadedFiles').style.display = 'block'; function addDocumentToExistingList(doc) {
} const existingList = document.getElementById('existingFilesList');
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
// Use PascalCase to match C# model properties
fileItem.innerHTML = `
<div class="file-info">
<i class="fas fa-file-pdf"></i>
<div>
<div><strong>${doc.FileName || 'Unnamed'}</strong> <small class="text-muted">(${doc.FileSize || 0} KB)</small></div>
${doc.DocumentDate ? `<small>Date: ${new Date(doc.DocumentDate).toLocaleDateString()}</small><br>` : ''}
${doc.RecipientName ? `<small>Recipient: ${doc.RecipientName}</small><br>` : ''}
${doc.SenderName ? `<small>Sender: ${doc.SenderName}</small><br>` : ''}
${doc.InvoiceNumber ? `<small>Invoice: ${doc.InvoiceNumber}</small><br>` : ''}
${doc.TotalAmount ? `<small>Amount: $${doc.TotalAmount}</small><br>` : ''}
${doc.ItemCount ? `<small>Items: ${doc.ItemCount}</small>` : ''}
</div>
</div>
<div class="file-actions">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="viewFile('${doc.FilePath}')">
<i class="fas fa-eye"></i> View
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteExistingFile(${doc.Id}, this)">
<i class="fas fa-trash"></i> Delete
</button>
</div>
`;
existingList.appendChild(fileItem);
}
function showUploadProgress(message) { function showUploadProgress(message) {
const progressDiv = document.getElementById('uploadProgress'); const progressDiv = document.getElementById('uploadProgress');

View File

@ -89,20 +89,68 @@
.ShowBorders(true) .ShowBorders(true)
.DataSource(list) .DataSource(list)
.KeyExpr("Id") .KeyExpr("Id")
.SearchPanel(sp => sp.Visible(true))
.HeaderFilter(hf => hf.Visible(true))
.Paging(p => p.PageSize(15))
.Pager(p => p.Visible(true))
.Editing(editing => {
editing.Mode(GridEditMode.Cell);
editing.AllowUpdating(true);
editing.AllowAdding(true);
editing.AllowDeleting(true);
})
.Columns(c => { .Columns(c => {
c.Add().DataField("Id"); c.Add().DataField("Id").AllowEditing(false);
c.Add().DataField("ShippingDate"); c.Add().DataField("ShippingDate");
c.Add().DataField("LicencePlate"); c.Add().DataField("LicencePlate");
c.Add().DataField("IsAllMeasured"); c.Add().DataField("IsAllMeasured");
}).MasterDetail(md => { })
.Toolbar(toolbar => {
toolbar.Items(items => {
items.Add()
.Name("addRowButton")
.ShowText(ToolbarItemShowTextMode.Always);
items.Add()
.Location(ToolbarItemLocation.After)
.Widget(w =>
w.Button()
.Text("Delete Selected Records")
.Icon("trash")
.Disabled(true)
.OnClick("onDeleteBtnClick")
);
});
})
.MasterDetail(md => {
md.Enabled(true); md.Enabled(true);
md.Template(@<text> md.Template(@<text>
<div class="master-detail-caption"><%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:</div> <div class="master-detail-caption"><%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:</div>
div id="fileuploader">
<div class="widget-container">
@(Html.DevExtreme().FileUploader()
.ID($"file-uploader-{new JS("data.Id")}")
.Name("myFile")
.Multiple(true)
.Accept("application/pdf")
.UploadMode(FileUploadMode.Instantly)
.UploadUrl(Url.Action("UploadFile", "Shipping"))
.OnValueChanged("fileUploader_valueChanged")
.OnUploaded("fileUploader_fileUploaded")
)
<div class="content" id="selected-files">
<div>
<h4>Selected Files</h4>
</div>
</div>
</div>
<div id="@($"shippingDocumentGridDiv-{new JS("data.Id")}")"></div>
@(Html.DevExtreme().DataGrid<FruitBank.Common.Entities.ShippingDocument>() @(Html.DevExtreme().DataGrid<FruitBank.Common.Entities.ShippingDocument>()
.ColumnAutoWidth(true) .ColumnAutoWidth(true)
.ShowBorders(true) .ShowBorders(true)
.ID($"shippingDocumentGridContainer-{new JS("data.Id")}")
.Columns(columns => { .Columns(columns => {
columns.AddFor(m => m.Id); columns.AddFor(m => m.Id).AllowEditing(false);
columns.AddFor(m => m.Country); columns.AddFor(m => m.Country);
@ -119,8 +167,9 @@
) )
</text>); </text>);
}) })
) )
)
</div> </div>
</section> </section>
@ -130,3 +179,84 @@
return rowData.Status === "Completed"; return rowData.Status === "Completed";
} }
</script> </script>
<script>
function onDeleteBtnClick(){
let dataGrid = $("#gridContainer").dxDataGrid("instance");
$.when.apply($, dataGrid.getSelectedRowsData().map(function(data) {
return dataGrid.getDataSource().store().remove(data.ID);
})).done(function() {
dataGrid.refresh();
});
}
function calculateFilterExpression(filterValue, selectedFilterOperation, target) {
if(target === "search" && typeof(filterValue) === "string") {
return [this.dataField, "contains", filterValue]
}
return function(data) {
return (data.AssignedEmployee || []).indexOf(filterValue) !== -1
}
}
function onSelectionChanged(data) {
let dataGrid = $("#gridContainer").dxDataGrid("instance");
dataGrid.option("toolbar.items[1].options.disabled", !data.selectedRowsData.length);
}
</script>
<script>
function fileUploader_valueChanged(e) {
var files = e.value;
if(files.length > 0) {
$("#selected-files .selected-item").remove();
$.each(files, function(i, file) {
var $selectedItem = $("<div />").addClass("selected-item");
$selectedItem.append(
$("<span />").html("Name: " + file.name + "<br/>"),
$("<span />").html("Size " + file.size + " bytes" + "<br/>"),
$("<span />").html("Type " + file.type + "<br/>"),
$("<span />").html("Last Modified Date: " + file.lastModifiedDate)
);
$selectedItem.appendTo($("#selected-files"));
});
$("#selected-files").show();
}
else
$("#selected-files").hide();
}
function getGridInstance() {
return $("#file-uploader").dxFileUploader("instance");
}
function fileUploader_fileUploaded(e) {
const fileUploaderId = e.component.element().attr('id');
// 2. Extract the number from the ID
const match = fileUploaderId.match(/\d+$/);
if (match) {
const uniqueId = match[0];
const gridId = `shippingDocumentGridContainer-${uniqueId}`;
// 3. Get the DevExtreme grid instance and refresh it
const grid = $(`#${gridId}`).dxDataGrid('instance');
if (grid) {
grid.dxDataGrid("getDataSource").reload();
// Optional: Show a success notification
DevExpress.ui.notify("Documents updated successfully!", "success", 2000);
} else {
console.error(`DevExtreme grid with ID "${gridId}" not found.`);
}
} else {
console.error("Could not find a unique ID number from the file uploader.");
}
// shippingDocumentGridContainer
//$("#shippingDocumentGridContainer" + e.component.ID).dxDataGrid("getDataSource").reload();
}
</script>

View File

@ -15,6 +15,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
/// </summary> /// </summary>
public string ModelName { get; set; } = "gpt-3.5-turbo"; public string ModelName { get; set; } = "gpt-3.5-turbo";
/// <summary>
/// Gets or sets the AI API key
/// </summary>
public string OpenAIApiKey { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the AI API model name
/// </summary>
public string OpenAIModelName { get; set; } = "gpt-3.5-turbo";
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the AI plugin is enabled /// Gets or sets a value indicating whether the AI plugin is enabled
/// </summary> /// </summary>
@ -25,6 +35,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
/// </summary> /// </summary>
public string ApiBaseUrl { get; set; } = "https://api.openai.com/v1"; public string ApiBaseUrl { get; set; } = "https://api.openai.com/v1";
/// <summary>
/// Gets or sets the API base URL (useful for different AI providers)
/// </summary>
public string OpenAIApiBaseUrl { get; set; } = "https://api.openai.com/v1";
/// <summary> /// <summary>
/// Gets or sets the maximum number of tokens for AI responses /// Gets or sets the maximum number of tokens for AI responses
/// </summary> /// </summary>

View File

@ -0,0 +1,271 @@
using System;
using System.Text.RegularExpressions;
using System.Text;
using System.Collections.Generic;
namespace Nop.Plugin.Misc.FruitBankPlugin.Helpers;
public static class TextHelper
{
// Special character replacement map
private static readonly Dictionary<string, string> HungarianSpecialCharacterMap = new()
{
{ "/", " per " },
{ "@", " kukac " },
{ "#", " kettőskereszt " },
{ "&", " és " },
//{ ",", " vessző " },
{ " = ", " egyenlő " }, // Example, you can add more
//{ " - ", " mínusz " } // Example, you can add more
};
private static readonly Dictionary<string, string> EnglishSpecialCharacterMap = new()
{
{ "/", " slash " },
{ "@", " at " },
{ "#", " hashtag " },
{ "&", " and " },
//{ ",", " vessző " },
{ " = ", " equals " }, // Example, you can add more
//{ " - ", " mínusz " } // Example, you can add more
};
public static string ReplaceNumbersAndSpecialCharacters(string text, string language)
{
// Save parts that should be skipped (emails, URLs, dates)
var protectedParts = new Dictionary<string, string>();
// Protect dates like 2024.05.06
text = Regex.Replace(text, @"\b\d{4}\.\d{2}\.\d{2}\b", match =>
{
string key = $"__DATE__{protectedParts.Count}__";
protectedParts[key] = match.Value;
return key;
});
// Remove anything between [] including the brackets themselves
text = Regex.Replace(text, @"\[[^\]]*\]", "");
// First replace floats (keep this BEFORE integers)
text = Regex.Replace(text, @"\b\d+\.\d+\b", match =>
{
var parts = match.Value.Split('.');
var integerPart = int.Parse(parts[0]);
var decimalPart = int.Parse(parts[1]);
if(language == "Hungarian")
{
return $"{NumberToHungarian(integerPart)} egész {NumberToHungarian(decimalPart)} {(parts[1].Length == 1 ? "tized" : parts[1].Length == 2 ? "század" : "ezred")}";
}
else
{
return $"{NumberToEnglish(integerPart)} point {NumberToEnglish(decimalPart)}";
}
});
// Then replace integers
text = Regex.Replace(text, @"\b\d+\b", match =>
{
int number = int.Parse(match.Value);
if(language == "Hungarian")
{
return NumberToHungarian(number);
}
else
{
return NumberToEnglish(number);
}
});
// Replace special characters from dictionary
if(language == "Hungarian")
{
foreach (var kvp in HungarianSpecialCharacterMap)
{
text = text.Replace(kvp.Key, kvp.Value);
}
}
else
{
foreach (var kvp in EnglishSpecialCharacterMap)
{
text = text.Replace(kvp.Key, kvp.Value);
}
}
// Replace dots surrounded by spaces (optional)
//text = Regex.Replace(text, @" (?=\.)|(?<=\.) ", " pont ");
// Restore protected parts
foreach (var kvp in protectedParts)
{
text = text.Replace(kvp.Key, kvp.Value);
}
return text;
}
public static string NumberToHungarian(int number)
{
if (number == 0) return "nulla";
string[] units = { "", "egy", "két", "három", "négy", "öt", "hat", "hét", "nyolc", "kilenc" };
string[] tens = { "", "tíz", "húsz", "harminc", "negyven", "ötven", "hatvan", "hetven", "nyolcvan", "kilencven" };
string[] tensAlternate = { "", "tizen", "huszon", "harminc", "negyven", "ötven", "hatvan", "hetven", "nyolcvan", "kilencven" };
StringBuilder result = new StringBuilder();
if (number >= 1000)
{
int thousands = number / 1000;
if (thousands == 1)
result.Append("ezer");
else
{
result.Append(NumberToHungarian(thousands));
result.Append("ezer");
}
number %= 1000;
}
if (number >= 100)
{
int hundreds = number / 100;
if (hundreds == 1)
result.Append("száz");
else
{
result.Append(NumberToHungarian(hundreds));
result.Append("száz");
}
number %= 100;
}
if (number >= 10)
{
int tensPart = number / 10;
result.Append(tensAlternate[tensPart]);
number %= 10;
}
if (number > 0)
{
// "két" instead of "kettő" in compound numbers
if (number == 2 && result.Length > 0)
result.Append("két");
else
result.Append(units[number]);
}
return result.ToString();
}
public static string NumberToEnglish(int number)
{
if (number == 0) return "zero";
string[] units = { "", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
string[] tens = { "", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninty" };
StringBuilder result = new StringBuilder();
if (number >= 1000)
{
int thousands = number / 1000;
if (thousands == 1)
result.Append("thousand");
else
{
result.Append(NumberToHungarian(thousands));
result.Append("thousand");
}
number %= 1000;
}
if (number >= 100)
{
int hundreds = number / 100;
if (hundreds == 1)
result.Append("hundred");
else
{
result.Append(NumberToHungarian(hundreds));
result.Append("hundred");
}
number %= 100;
}
if (number >= 10)
{
//int tensPart = number / 10;
//result.Append(tens[tensPart]);
//number %= 10;
switch (number)
{
case 10:
result.Append("ten");
break;
case 11:
result.Append("eleven");
break;
case 12:
result.Append("twelve");
break;
case 13:
result.Append("thirteen");
break;
case 14:
result.Append("fourteen");
break;
case 15:
result.Append("fifteen");
break;
case 16:
result.Append("sixteen");
break;
case 17:
result.Append("seventeen");
break;
case 18:
result.Append("eighteen");
break;
case 19:
result.Append("nineteen");
break;
}
}
return result.ToString();
}
public static string FixJsonWithoutAI(string aiResponse)
{
if (aiResponse.StartsWith("```"))
{
//Console.WriteLine("FIXING ``` in AI Response.");
aiResponse = aiResponse.Substring(3);
if (aiResponse.StartsWith("json"))
{
aiResponse = aiResponse.Substring(4);
}
if (aiResponse.StartsWith("html"))
{
aiResponse = aiResponse.Substring(4);
}
aiResponse = aiResponse.Substring(0, aiResponse.Length - 3);
}
return aiResponse;
}
public static string RemoveTabs(string text)
{
if (string.IsNullOrEmpty(text)) return text;
return text.Replace("\t", ""); // Simple replace — remove all tab characters
}
}

View File

@ -75,6 +75,7 @@ public class PluginNopStartup : INopStartup
services.AddScoped<IOrderModelFactory, CustomOrderModelFactory>(); services.AddScoped<IOrderModelFactory, CustomOrderModelFactory>();
services.AddScoped<IGenericAttributeService, GenericAttributeService>(); services.AddScoped<IGenericAttributeService, GenericAttributeService>();
services.AddScoped<CerebrasAPIService>(); services.AddScoped<CerebrasAPIService>();
services.AddScoped<OpenAIApiService>();
//services.AddScoped<IAIAPIService, OpenAIApiService>(); //services.AddScoped<IAIAPIService, OpenAIApiService>();
services.AddScoped<AICalculationService>(); services.AddScoped<AICalculationService>();
services.AddControllersWithViews(options => services.AddControllersWithViews(options =>

View File

@ -81,6 +81,11 @@ public class RouteProvider : IRouteProvider
name: "Plugin.FruitBank.Admin.Shipping.UploadFile", name: "Plugin.FruitBank.Admin.Shipping.UploadFile",
pattern: "Admin/Shipping/UploadFile", pattern: "Admin/Shipping/UploadFile",
defaults: new { controller = "Shipping", action = "UploadFile", area = AreaNames.ADMIN }); defaults: new { controller = "Shipping", action = "UploadFile", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Shipping.ReloadPartialView",
pattern: "Admin/Shipping/ReloadPartialView",
defaults: new { controller = "Shipping", action = "ReloadPartialView", area = AreaNames.ADMIN });
} }
/// <summary> /// <summary>

View File

@ -146,6 +146,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="Areas\Admin\Components\_DocumentsGridPartial.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Components\_WelcomeMessage.cshtml"> <None Update="Areas\Admin\Components\_WelcomeMessage.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>

View File

@ -2,6 +2,7 @@
using Nop.Core; using Nop.Core;
using Nop.Core.Domain.Customers; using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Stores; using Nop.Core.Domain.Stores;
using Nop.Plugin.Misc.FruitBankPlugin.Helpers;
using StackExchange.Redis; using StackExchange.Redis;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -14,6 +15,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
public class AICalculationService public class AICalculationService
{ {
private readonly CerebrasAPIService _cerebrasApiService; private readonly CerebrasAPIService _cerebrasApiService;
private readonly OpenAIApiService _openAIApiService;
private readonly IStoreContext _storeContext; private readonly IStoreContext _storeContext;
public AICalculationService(CerebrasAPIService cerebrasApiService, IStoreContext storeContext) public AICalculationService(CerebrasAPIService cerebrasApiService, IStoreContext storeContext)
{ {
@ -33,5 +35,18 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
var response = await _cerebrasApiService.GetSimpleResponseAsync(systemMessage, userMessage); var response = await _cerebrasApiService.GetSimpleResponseAsync(systemMessage, userMessage);
return response; return response;
} }
public async Task<string> GetOpenAIPDFAnalyzisFromText(string pdfText, string userQuestion)
{
string systemMessage = $"You are a helpful assistant of a webshop, you work in the administration area, with the ADMIN user. The ADMIN user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
var response = await _cerebrasApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
var fixedResponse = TextHelper.FixJsonWithoutAI(response);
return fixedResponse;
}
} }
} }

View File

@ -30,8 +30,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {GetApiKey()}"); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {GetApiKey()}");
} }
public string GetApiKey() => _fruitBankSettings.ApiKey; public string GetApiKey() => _fruitBankSettings.OpenAIApiKey;
public string GetModelName() => _fruitBankSettings.ModelName; public string GetModelName() => _fruitBankSettings.OpenAIModelName;
public void RegisterCallback(Action<string, string> callback, Action<string> onCompleteCallback, Action<string, string> onErrorCallback) public void RegisterCallback(Action<string, string> callback, Action<string> onCompleteCallback, Action<string, string> onErrorCallback)
{ {