This commit is contained in:
Loretta 2025-09-30 18:18:44 +02:00
commit 63abc01006
183 changed files with 888901 additions and 380 deletions

Binary file not shown.

View File

@ -1,47 +0,0 @@
{
"Version": 1,
"WorkspaceRootPath": "H:\\Applications\\Mango\\Source\\NopCommerce.Common\\4.70\\Plugins\\",
"Documents": [],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": -1,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:129:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:129:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:128:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:130:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:131:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:132:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:128:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
}
]
}
]
}
]
}

View File

@ -1,7 +0,0 @@
{
"ExpandedNodes": [
""
],
"SelectedNode": "\\H:\\Applications\\Mango\\Source\\NopCommerce.Common\\4.70\\Plugins",
"PreviewInSolutionExplorer": false
}

View File

@ -0,0 +1,58 @@
@using Nop.Core;
@using Nop.Plugin.Misc.FruitBankPlugin.Services
@using Nop.Core.Domain.Customers
@inject IWorkContext workContext
@inject AICalculationService aiCalculationService
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-leaf"></i>
Üdvözöljük a Fruit Bank rendszerben!
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
@{
Customer customer = await workContext.GetCurrentCustomerAsync();
var WelcomeMessage = await aiCalculationService.GetWelcomeMessageAsync(customer);
var email = customer.Email;
<h4>Ssytem check</h4>
<p class="lead">
@WelcomeMessage
</p>
<p class="lead">
Itt kezelheti az összes terméket, rendelést és ügyfelet egyszerűen és hatékonyan.
</p>
}
<p>
Mai dátum: <strong>@DateTime.Now.ToString("yyyy. MMMM dd., dddd", new System.Globalization.CultureInfo("hu-HU"))</strong>
</p>
@*
<hr>
<div class="row">
<div class="col-sm-6">
<h5><i class="fas fa-apple-alt text-success"></i> Gyümölcsök</h5>
<p class="text-muted">Friss, minőségi gyümölcsök széles választéka</p>
</div>
<div class="col-sm-6">
<h5><i class="fas fa-shipping-fast text-primary"></i> Gyors szállítás</h5>
<p class="text-muted">Megbízható kiszállítás országszerte</p>
</div>
</div> *@
</div>
<div class="col-md-4">
<div class="bg-light p-3 rounded">
<h5><i class="fas fa-chart-line text-warning"></i> Mai összefoglaló</h5>
<ul class="list-unstyled">
<li><i class="fas fa-clock text-info"></i> Bejelentkezés ideje: @DateTime.Now.ToString("HH:mm")</li>
<li><i class="fas fa-calendar text-success"></i> Aktív napok: @DateTime.Now.DayOfYear</li>
<li><i class="fas fa-sun text-warning"></i> Szép napot kívánunk!</li>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@ -63,6 +63,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
//display a warning to a store owner if there are some error
var customer = await _workContext.GetCurrentCustomerAsync();
var email = customer.Email;
var hideCard = await _genericAttributeService.GetAttributeAsync<bool>(customer, NopCustomerDefaults.HideConfigurationStepsAttribute);
var closeCard = await _genericAttributeService.GetAttributeAsync<bool>(customer, NopCustomerDefaults.CloseConfigurationStepsAttribute);

View File

@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
using Nop.Plugin.Misc.FruitBankPlugin;
//using Nop.Plugin.Misc.FruitBankPlugin;
using Nop.Services.Configuration;
using Nop.Web.Framework.Controllers;
using Nop.Services.Messages;
using System.Threading.Tasks;
namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
{
@ -13,9 +13,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
{
private readonly INotificationService _notificationService;
private readonly ISettingService _settingService;
private readonly OpenAiSettings _settings;
private readonly FruitBankSettings _settings;
public FruitBankPluginAdminController(INotificationService notificationService, ISettingService settingService, OpenAiSettings settings)
public FruitBankPluginAdminController(INotificationService notificationService, ISettingService settingService, FruitBankSettings settings)
{
_notificationService = notificationService;
_settingService = settingService;
@ -27,7 +27,13 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
{
var model = new ConfigureModel
{
ApiKey = _settings.ApiKey
ApiKey = _settings.ApiKey,
ModelName = _settings.ModelName,
IsEnabled = _settings.IsEnabled,
ApiBaseUrl = _settings.ApiBaseUrl,
MaxTokens = _settings.MaxTokens,
Temperature = _settings.Temperature,
RequestTimeoutSeconds = _settings.RequestTimeoutSeconds
};
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Configure/Configure.cshtml", model);
}
@ -35,11 +41,25 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
[HttpPost]
public async Task<IActionResult> Configure(ConfigureModel model)
{
_settings.ApiKey = model.ApiKey;
if (!ModelState.IsValid)
{
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Configure/Configure.cshtml", model);
}
// Map model properties to settings
_settings.ApiKey = model.ApiKey ?? string.Empty;
_settings.ModelName = model.ModelName ?? string.Empty;
_settings.IsEnabled = model.IsEnabled;
_settings.ApiBaseUrl = model.ApiBaseUrl ?? string.Empty;
_settings.MaxTokens = model.MaxTokens;
_settings.Temperature = model.Temperature;
_settings.RequestTimeoutSeconds = model.RequestTimeoutSeconds;
// Save settings
await _settingService.SaveSettingAsync(_settings);
_notificationService.SuccessNotification("Beállítások mentve.");
return RedirectToAction("Configure");
}
}
}
}

View File

@ -1,149 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Nop.Web.Areas.Admin.Controllers;
using Nop.Web.Framework.Mvc.Filters;
using Nop.Web.Framework;
using Nop.Services.Security;
using Microsoft.AspNetCore.Http;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
using Nop.Services.Messages;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
[Area(AreaNames.ADMIN)]
[AuthorizeAdmin]
public class ShipmentController : BaseAdminController
{
private readonly IPermissionService _permissionService;
protected readonly INotificationService _notificationService;
public ShipmentController(IPermissionService permissionService, INotificationService notificationService)
{
_permissionService = permissionService;
_notificationService = notificationService;
}
public async Task<IActionResult> List()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipment/List.cshtml");
}
[HttpGet]
public async Task<IActionResult> Create()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
var model = new CreateShipmentModel
{
ShipmentDate = DateTime.Now
};
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipment/Create.cshtml", model);
}
[HttpPost]
public async Task<IActionResult> Create(CreateShipmentModel model)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
if (!ModelState.IsValid)
{
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipment/Create.cshtml", model);
}
try
{
// TODO: Save shipment to database
// var shipment = new Shipment
// {
// Name = model.ShipmentName,
// Description = model.Description,
// ShipmentDate = model.ShipmentDate,
// TrackingNumber = model.TrackingNumber,
// CreatedOnUtc = DateTime.UtcNow
// };
// await _shipmentService.InsertShipmentAsync(shipment);
_notificationService.SuccessNotification("Shipment created successfully");
return RedirectToAction("List");
}
catch (Exception ex)
{
_notificationService.ErrorNotification($"Error creating shipment: {ex.Message}");
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipment/Create.cshtml", model);
}
}
[HttpPost]
public async Task<IActionResult> UploadFile(IFormFile file)
{
try
{
if (file == null || file.Length == 0)
return Json(new FileUploadResult { Success = false, ErrorMessage = "No file selected" });
// Validate file type (PDF only)
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
return Json(new FileUploadResult { Success = false, ErrorMessage = "Only PDF files are allowed" });
// Validate file size (e.g., max 10MB)
if (file.Length > 10 * 1024 * 1024)
return Json(new FileUploadResult { Success = false, ErrorMessage = "File size must be less than 10MB" });
// Create upload directory if it doesn't exist
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shipments");
Directory.CreateDirectory(uploadsPath);
// Generate unique filename
var fileName = $"{Guid.NewGuid()}_{file.FileName}";
var filePath = Path.Combine(uploadsPath, fileName);
// Save file
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Json(new FileUploadResult
{
Success = true,
FileName = file.FileName,
FilePath = $"/uploads/shipments/{fileName}"
});
}
catch (Exception ex)
{
return Json(new FileUploadResult { Success = false, ErrorMessage = ex.Message });
}
}
[HttpPost]
public IActionResult DeleteUploadedFile(string filePath)
{
try
{
if (string.IsNullOrEmpty(filePath))
return Json(new { success = false, message = "Invalid file path" });
var fullPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", filePath.TrimStart('/'));
if (System.IO.File.Exists(fullPath))
{
System.IO.File.Delete(fullPath);
return Json(new { success = true });
}
return Json(new { success = false, message = "File not found" });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
}
}

View File

@ -0,0 +1,392 @@
using FruitBank.Common.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Services.Messages;
using Nop.Services.Security;
using Nop.Web.Areas.Admin.Controllers;
using Nop.Web.Areas.Admin.Models.Orders;
using Nop.Web.Framework;
using Nop.Web.Framework.Models;
using Nop.Web.Framework.Models.Extensions;
using Nop.Web.Framework.Mvc.Filters;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
[Area(AreaNames.ADMIN)]
[AuthorizeAdmin]
public class ShippingController : BaseAdminController
{
private readonly IPermissionService _permissionService;
protected readonly INotificationService _notificationService;
protected readonly ShippingItemDbTable _shippingItemDbTable;
protected readonly ShippingDbTable _shippingDbTable;
protected readonly ShippingDocumentDbTable _shippingDocumentDbTable;
protected readonly FruitBankDbContext _dbContext;
//private readonly IFruitBankShippingModelFactory _shippingModelFactory;
// TODO: Add your shipment and document services here
// private readonly IShipmentService _shipmentService;
// private readonly IShipmentDocumentService _documentService;
public ShippingController(IPermissionService permissionService, INotificationService notificationService, ShippingItemDbTable shippingItemDbTable, ShippingDbTable shippingDbTable, ShippingDocumentDbTable shippingDocumentDbTable, FruitBankDbContext dbContext)
{
_permissionService = permissionService;
_notificationService = notificationService;
_shippingItemDbTable = shippingItemDbTable;
_shippingDbTable = shippingDbTable;
_shippingDocumentDbTable = shippingDocumentDbTable;
_dbContext = dbContext;
//_shippingModelFactory = shippingModelFactory;
}
[HttpGet]
public async Task<IActionResult> List()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
// Create model and load data
var model = new ShippingListModel();
// TODO: Replace with your actual service call
// model.ShippingList = await _shippingService.GetAllShippingsAsync();
// Mock data for now
model.ShippingList = _dbContext.Shippings.GetAll(true).ToList();
var valami = model;
//model. = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId);
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/List.cshtml", model);
}
[HttpPost]
public async Task<IActionResult> List(ShippingListModel model)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
// Apply filters to mock data
model.ShippingList = _shippingDbTable.GetAll().ToList();
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/List.cshtml", model);
}
[HttpGet]
public async Task<IActionResult> ShippingDocumentList()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
// Create model and load data
var model = _shippingDocumentDbTable.GetAll().ToList();
// TODO: Replace with your actual service call
// model.ShippingList = await _shippingService.GetAllShippingsAsync();
// Mock data for now
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/List.cshtml", model);
}
[HttpPost]
public async Task<IActionResult> ShippingDocumentList(int shippingId)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
// Apply filters to mock data
var model = await _dbContext.GetShippingDocumentsByShippingIdAsync(shippingId);
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/List.cshtml", model);
}
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Json(new { success = false, message = "Access denied" });
try
{
// TODO: Implement actual deletion
return Json(new { success = true, message = "Shipment deleted successfully" });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpGet]
public async Task<IActionResult> Create()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
var model = new CreateShippingModel
{
ShippingDate = DateTime.Now
};
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/Create.cshtml", model);
}
[HttpPost]
public async Task<IActionResult> Create(CreateShippingModel model)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
if (!ModelState.IsValid)
{
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/Create.cshtml", model);
}
string licencePlate = "";
if (model.LicencePlate.Length > 3)
{
licencePlate = model.LicencePlate?.Trim().ToUpper() ?? string.Empty;
}
try
{
var shipment = new Shipping
{
LicencePlate = licencePlate,
ShippingDate = model.ShippingDate,
IsAllMeasured = false,
};
await _shippingDbTable.InsertAsync(shipment);
_notificationService.SuccessNotification($"Shipment created successfully. You can now upload documents. Shipping Id: {shipment.Id}");
// Redirect to Edit action where user can upload files
return RedirectToAction("Edit", new { id = shipment.Id });
}
catch (Exception ex)
{
_notificationService.ErrorNotification($"Error creating shipment: {ex.Message}");
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/Create.cshtml", model);
}
}
[HttpGet]
public async Task<IActionResult> Edit(int id)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
try
{
// TODO: Load shipment from database
var shipment = await _shippingDbTable.GetByIdAsync(id);
if (shipment == null)
return RedirectToAction("List");
// TODO: Load existing documents
// var documents = await _documentService.GetShipmentDocumentsAsync(id);
// For now, create a mock model
var model = new EditShippingModel
{
Id = shipment.Id,
ShippingDate = shipment.ShippingDate, // Replace with: shipment.ShipmentDate
LicencePlate = shipment.LicencePlate, // Replace with: shipment.TrackingNumber
ExistingDocuments = new List<ShippingDocumentModel>()
// Replace with: documents.Select(d => new ShipmentDocumentModel { ... }).ToList()
};
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/Edit.cshtml", model);
}
catch (Exception ex)
{
_notificationService.ErrorNotification($"Error loading shipment: {ex.Message}");
return RedirectToAction("List");
}
}
[HttpPost]
public async Task<IActionResult> Edit(EditShippingModel model)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
if (!ModelState.IsValid)
{
// Reload existing documents if validation fails
// model.ExistingDocuments = await LoadExistingDocuments(model.Id);
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/Edit.cshtml", model);
}
try
{
// TODO: Update shipment in database
// var shipment = await _shipmentService.GetShipmentByIdAsync(model.Id);
// shipment.Name = model.ShipmentName;
// shipment.Description = model.Description;
// shipment.ShipmentDate = model.ShipmentDate;
// shipment.TrackingNumber = model.TrackingNumber;
// await _shipmentService.UpdateShippingAsync(shipment);
_notificationService.SuccessNotification("Shipment updated successfully");
return RedirectToAction("Edit", new { id = model.Id });
}
catch (Exception ex)
{
_notificationService.ErrorNotification($"Error updating shipment: {ex.Message}");
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Shipping/Edit.cshtml", model);
}
}
[HttpPost]
public async Task<IActionResult> UploadFile(IFormFile file, int shipmentId)
{
try
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Json(new FileUploadResult { Success = false, ErrorMessage = "Access denied" });
if (file == null || file.Length == 0)
return Json(new FileUploadResult { Success = false, ErrorMessage = "No file selected" });
// Validate file type (PDF only)
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
return Json(new FileUploadResult { Success = false, ErrorMessage = "Only PDF files are allowed" });
// Validate file size (e.g., max 10MB)
if (file.Length > 10 * 1024 * 1024)
return Json(new FileUploadResult { Success = false, ErrorMessage = "File size must be less than 10MB" });
// Create upload directory if it doesn't exist
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shipments");
Directory.CreateDirectory(uploadsPath);
// Generate unique filename
var fileName = $"{Guid.NewGuid()}_{file.FileName}";
var filePath = Path.Combine(uploadsPath, fileName);
// Save file
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
// TODO: Save document record to database
// var document = new ShipmentDocument
// {
// ShipmentId = shipmentId,
// FileName = file.FileName,
// FilePath = $"/uploads/shipments/{fileName}",
// FileSize = (int)(file.Length / 1024), // Convert to KB
// ContentType = file.ContentType,
// UploadDate = DateTime.UtcNow,
// IsActive = true
// };
// var savedDocument = await _documentService.InsertDocumentAsync(document);
return Json(new FileUploadResult
{
Success = true,
FileName = file.FileName,
FilePath = $"/uploads/shippingDocuments/{fileName}",
FileSize = (int)(file.Length / 1024), // KB
DocumentId = 1 // Replace with: savedDocument.Id
});
}
catch (Exception ex)
{
return Json(new FileUploadResult { Success = false, ErrorMessage = ex.Message });
}
}
[HttpPost]
public async Task<IActionResult> DeleteUploadedFile(string filePath)
{
try
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Json(new { success = false, message = "Access denied" });
if (string.IsNullOrEmpty(filePath))
return Json(new { success = false, message = "Invalid file path" });
// TODO: Delete document record from database first
// var document = await _documentService.GetDocumentByFilePathAsync(filePath);
// if (document != null)
// await _documentService.DeleteDocumentAsync(document);
var fullPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", filePath.TrimStart('/'));
if (System.IO.File.Exists(fullPath))
{
System.IO.File.Delete(fullPath);
return Json(new { success = true });
}
return Json(new { success = false, message = "File not found" });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpPost]
public async Task<IActionResult> DeleteDocument(int documentId)
{
try
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Json(new DocumentOperationResult { Success = false, Message = "Access denied" });
// TODO: Implement document deletion
// var document = await _documentService.GetDocumentByIdAsync(documentId);
// if (document == null)
// return Json(new DocumentOperationResult { Success = false, Message = "Document not found" });
// Delete physical file
// var fullPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", document.FilePath.TrimStart('/'));
// if (System.IO.File.Exists(fullPath))
// System.IO.File.Delete(fullPath);
// Delete database record
// await _documentService.DeleteDocumentAsync(document);
return Json(new DocumentOperationResult
{
Success = true,
Message = "Document deleted successfully",
DocumentId = documentId
});
}
catch (Exception ex)
{
return Json(new DocumentOperationResult { Success = false, Message = ex.Message });
}
}
// Helper method for loading existing documents (to be implemented)
// private async Task<List<ShipmentDocumentModel>> LoadExistingDocuments(int shipmentId)
// {
// var documents = await _documentService.GetShipmentDocumentsAsync(shipmentId);
// return documents.Select(d => new ShipmentDocumentModel
// {
// Id = d.Id,
// ShipmentId = d.ShipmentId,
// FileName = d.FileName,
// FilePath = d.FilePath,
// FileSize = d.FileSize,
// UploadDate = d.UploadDate,
// ContentType = d.ContentType,
// IsActive = d.IsActive
// }).ToList();
// }
}
}

View File

@ -10,7 +10,25 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
public record ConfigureModel
{
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiKey")]
public string ApiKey { get; set; }
public string ApiKey { get; set; } = string.Empty;
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ModelName")]
public string ModelName { get; set; } = string.Empty;
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.IsEnabled")]
public bool IsEnabled { get; set; }
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.ApiBaseUrl")]
public string ApiBaseUrl { get; set; } = string.Empty;
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.MaxTokens")]
public int MaxTokens { get; set; }
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.Temperature")]
public decimal Temperature { get; set; }
[NopResourceDisplayName("Plugins.FruitBankPlugin.Fields.RequestTimeoutSeconds")]
public int RequestTimeoutSeconds { get; set; }
}
}

View File

@ -4,19 +4,17 @@ using System.ComponentModel.DataAnnotations;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
{
public record CreateShipmentModel : BaseNopModel
{
[Required]
public string ShipmentName { get; set; }
public string Description { get; set; }
public record CreateShippingModel
{
[Required]
public DateTime ShipmentDate { get; set; } = DateTime.Now;
public DateTime ShippingDate { get; set; } = DateTime.Now;
public string TrackingNumber { get; set; }
public string LicencePlate { get; set; }
public List<string> UploadedFiles { get; set; } = new();
public bool IsAllMeasured { get; set; } = false;
// Removed UploadedFiles since file upload happens only in edit mode
}
public class FileUploadResult
@ -25,5 +23,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
public string FileName { get; set; }
public string FilePath { get; set; }
public string ErrorMessage { get; set; }
public int FileSize { get; set; }
public int DocumentId { get; set; } // For tracking the document ID after upload
}
}

View File

@ -0,0 +1,58 @@
using FruitBank.Common.Entities;
using Microsoft.AspNetCore.Http;
using Nop.Web.Framework.Models;
using System.ComponentModel.DataAnnotations;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
{
public record EditShippingModel : BaseNopModel
{
public int Id { get; set; }
public string LicencePlate { get; set; }
[Required]
public DateTime ShippingDate { get; set; }
public bool IsAllMeasured { get; set; }
// List of existing documents associated with this Shipping
public List<ShippingDocumentModel> ExistingDocuments { get; set; } = new();
// For tracking newly uploaded files during the session
public List<string> NewlyUploadedFiles { get; set; } = new();
}
public class ShippingDocumentModel
{
public int Id { get; set; }
public ShippingDocument ShippingDocument { get; set; }
public string FileName { get; set; }
public string FilePath { get; set; }
public int FileSize { get; set; } // in KB
public DateTime UploadDate { get; set; }
public string ContentType { get; set; }
public bool IsActive { get; set; } = true;
// 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
public class DocumentOperationResult
{
public bool Success { get; set; }
public string Message { get; set; }
public ShippingDocumentModel Document { get; set; }
public int DocumentId { get; set; }
}
// Model for bulk operations
public class BulkDocumentOperation
{
public List<int> DocumentIds { get; set; } = new();
public string Operation { get; set; } // "delete", "activate", "deactivate"
}
}

View File

@ -0,0 +1,32 @@
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 ShippingListModel : BaseNopModel
{
public ShippingListModel()
{
ShippingList = new List<Shipping>();
}
public List<Shipping> ShippingList { get; set; }
// Search filters
[NopResourceDisplayName("Admin.FruitBankPlugin.Shipping.SearchLicencePlate")]
public string SearchLicencePlate { get; set; } = string.Empty;
[NopResourceDisplayName("Admin.FruitBankPlugin.Shipping.SearchDateFrom")]
public DateTime? SearchDateFrom { get; set; }
[NopResourceDisplayName("Admin.FruitBankPlugin.Shipping.SearchDateTo")]
public DateTime? SearchDateTo { get; set; }
[NopResourceDisplayName("Admin.FruitBankPlugin.Shipping.SearchPartnerId")]
public int SearchPartnerId { get; set; }
[NopResourceDisplayName("Admin.FruitBankPlugin.Shipping.SearchIsAllMeasured")]
public int SearchIsAllMeasured { get; set; } // 0 = All, 1 = Yes, 2 = No
}
}

View File

@ -0,0 +1,46 @@
//using Nop.Web.Framework.Models;
//using Nop.Web.Framework.Mvc.ModelBinding;
//using System.ComponentModel.DataAnnotations;
//namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
//{
// /// <summary>
// /// Represents a shipment search model
// /// </summary>
// public partial record ShippingModel : BaseNopEntityModel
// {
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.Id")]
// public override int Id { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.PartnerId")]
// public int PartnerId { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.PartnerName")]
// public string PartnerName { get; set; } = string.Empty;
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.ShippingDate")]
// public DateTime ShippingDate { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.LicencePlate")]
// public string LicencePlate { get; set; } = string.Empty;
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.IsAllMeasured")]
// public bool IsAllMeasured { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.DocumentCount")]
// public int DocumentCount { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.Created")]
// public DateTime Created { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.Fields.Modified")]
// public DateTime Modified { get; set; }
// // Computed properties for display
// public string FormattedShippingDate => ShippingDate.ToString("yyyy-MM-dd");
// public string FormattedCreated => Created.ToString("yyyy-MM-dd HH:mm");
// public string FormattedModified => Modified.ToString("yyyy-MM-dd HH:mm");
// public string IsAllMeasuredText => IsAllMeasured ? "Yes" : "No";
// public string StatusBadgeClass => IsAllMeasured ? "badge-success" : "badge-warning";
// }
//}

View File

@ -0,0 +1,34 @@
//using Nop.Web.Framework.Models;
//using Nop.Web.Framework.Mvc.ModelBinding;
//using System.ComponentModel.DataAnnotations;
//namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
//{
// /// <summary>
// /// Represents a shipment search model
// /// </summary>
// public partial record ShippingSearchModel : BaseSearchModel
// {
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.List.SearchShipmentDate")]
// public DateTime? SearchShippingDateFrom { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.List.SearchShipmentDate")]
// public DateTime? SearchShippingDateTo { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.List.SearchLicencePlate")]
// public string SearchLicencePlate { get; set; } = string.Empty;
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.List.SearchPartnerId")]
// public int? SearchPartnerId { get; set; }
// [NopResourceDisplayName("Admin.FruitBankPlugin.Shipments.List.SearchIsAllMeasured")]
// public int? SearchIsAllMeasured { get; set; } // 0 = All, 1 = Yes, 2 = No
// public bool HideSearchBlock { get; set; }
// public ShippingSearchModel()
// {
// SetGridPageSize();
// }
// }
//}

View File

@ -1,22 +1,99 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.ConfigureModel
@{
Layout = "_AdminLayout";
NopHtml.SetActiveMenuItemSystemName("AiAssistant.Configure");
NopHtml.SetActiveMenuItemSystemName("FruitBankPlugin.Configure");
}
<div class="card">
<div class="card-header">
<h2>AI Assistant Plugin - Beállítások</h2>
<h2>FruitBank AI Plugin - Beállítások</h2>
</div>
<div class="card-body">
<form asp-controller="AiAssistantAdmin" asp-action="Configure" method="post">
<form asp-controller="FruitBankPluginAdmin" asp-action="Configure" method="post">
<div class="form-group">
<div class="form-check">
<input asp-for="IsEnabled" class="form-check-input" />
<label asp-for="IsEnabled" class="form-check-label"></label>
<span asp-validation-for="IsEnabled" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ApiKey"></label>
<input asp-for="ApiKey" class="form-control" />
<input asp-for="ApiKey" class="form-control" type="password" placeholder="Adja meg az AI API kulcsot" />
<span asp-validation-for="ApiKey" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ModelName"></label>
<input asp-for="ModelName" class="form-control" placeholder="pl. gpt-3.5-turbo, gpt-4" />
<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>
</div>
<div class="form-group">
<label asp-for="ApiBaseUrl"></label>
<input asp-for="ApiBaseUrl" class="form-control" placeholder="https://api.openai.com/v1" />
<span asp-validation-for="ApiBaseUrl" 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="col-md-4">
<div class="form-group">
<label asp-for="MaxTokens"></label>
<input asp-for="MaxTokens" class="form-control" type="number" min="1" max="4000" />
<span asp-validation-for="MaxTokens" class="text-danger"></span>
<small class="form-text text-muted">Maximum token szám (1-4000)</small>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label asp-for="Temperature"></label>
<input asp-for="Temperature" class="form-control" type="number" step="0.1" min="0" max="1" />
<span asp-validation-for="Temperature" class="text-danger"></span>
<small class="form-text text-muted">Kreativitás (0.0-1.0)</small>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label asp-for="RequestTimeoutSeconds"></label>
<input asp-for="RequestTimeoutSeconds" class="form-control" type="number" min="5" max="300" />
<span asp-validation-for="RequestTimeoutSeconds" class="text-danger"></span>
<small class="form-text text-muted">Időtúllépés másodpercben (5-300)</small>
</div>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Mentés
</button>
<a asp-controller="Plugin" asp-action="List" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Vissza a pluginokhoz
</a>
</div>
<button type="submit" class="btn btn-primary">Mentés</button>
</form>
</div>
</div>
<script>
$(document).ready(function() {
// Show/hide advanced settings based on IsEnabled
function toggleAdvancedSettings() {
var isEnabled = $('#IsEnabled').is(':checked');
if (isEnabled) {
$('.form-group:not(:first)').show();
} else {
$('.form-group:not(:first)').hide();
}
}
$('#IsEnabled').change(toggleAdvancedSettings);
toggleAdvancedSettings();
});
</script>

View File

@ -14,7 +14,7 @@
const string closeCardAttributeName = "CloseConfigurationSteps";
var closeConfigurationStepsCard = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), closeCardAttributeName);
//active menu item (system name)
//active menu item (system name)
NopHtml.SetActiveMenuItemSystemName("Dashboard");
}
@ -31,21 +31,14 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@if (!closeConfigurationStepsCard)
{
<div class="row">
<div class="col-md-12">
@await Html.PartialAsync("~/Areas/Admin/Views/Home/_ConfigurationSteps.cshtml")
</div>
</div>
}
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.DashboardTop, additionalData = Model })
@if (!Model.IsLoggedInAsVendor)
{
<div class="row">
<div class="col-md-12">
@await Component.InvokeAsync(typeof(NopCommerceNewsViewComponent))
@await Html.PartialAsync("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Components/_WelcomeMessage.cshtml")
</div>
</div>
}
@ -125,5 +118,5 @@
</div>
</div>
</section>
<nop-alert asp-alert-id="loadOrderStatisticsAlert" asp-alert-message="@T("Admin.SalesReport.OrderStatistics.Alert.FailedLoad")" />
<nop-alert asp-alert-id="loadCustomerStatisticsAlert" asp-alert-message="@T("Admin.Reports.Customers.CustomerStatistics.Alert.FailedLoad")" />
<nop-alert asp-alert-id="loadOrderStatisticsAlert" asp-alert-message="@T("Admin.SalesReport.OrderStatistics.Alert.FailedLoad")" />
<nop-alert asp-alert-id="loadCustomerStatisticsAlert" asp-alert-message="@T("Admin.Reports.Customers.CustomerStatistics.Alert.FailedLoad")" />

View File

@ -1,32 +0,0 @@
@{
// Layout = "_AdminLayout";
ViewBag.PageTitle = "Shipments";
NopHtml.SetActiveMenuItemSystemName("Shipments");
}
<div class="content-header clearfix">
<h1 class="float-left">
<i class="fas fa-shipping-fast"></i>
Shipments
</h1>
</div>
<section class="content">
<div class="container-fluid">
<div class="form-horizontal">
<div class="cards-group">
<div class="card card-default">
<div class="card-header">
<h3 class="card-title">
Shipment Management
</h3>
</div>
<div class="card-body">
<p>This is our custom Shipments page.</p>
<p>Add shipment functionality here.</p>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,84 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.CreateShippingModel
@{
// Layout = "_AdminLayout";
ViewBag.PageTitle = "Create Shipping";
NopHtml.SetActiveMenuItemSystemName("Shippings.Create");
}
<form asp-action="Create" method="post">
<div class="content-header clearfix">
<h1 class="float-left">
<i class="fas fa-plus"></i>
Create New Shipment
</h1>
<div class="float-right">
<button type="submit" name="save" class="btn btn-primary">
<i class="far fa-save"></i>
Save Shipment
</button>
<a asp-action="List" class="btn btn-default">
<i class="fas fa-arrow-left"></i>
Back to List
</a>
</div>
</div>
<section class="content">
<div class="container-fluid">
<div class="form-horizontal">
<div class="cards-group">
<!-- Basic Information -->
<div class="card card-default">
<div class="card-header">
<div class="card-title">Shipping Information</div>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="LicencePlate" />
</div>
<div class="col-md-9">
<nop-editor asp-for="LicencePlate" required="true" />
<span asp-validation-for="LicencePlate"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="ShippingDate" />
</div>
<div class="col-md-9">
<nop-editor asp-for="ShippingDate" asp-template="DateTime" required="true" />
<span asp-validation-for="ShippingDate"></span>
</div>
</div>
</div>
</div>
<!-- Info card about next steps -->
<div class="card card-info">
<div class="card-header">
<div class="card-title">
<i class="fas fa-info-circle"></i>
Next Steps
</div>
</div>
<div class="card-body">
<p><strong>After saving this shipping:</strong></p>
<ul>
<li><i class="fas fa-check text-success"></i> The shipping will be created in the system</li>
<li><i class="fas fa-upload text-primary"></i> You'll be able to upload PDF documents</li>
<li><i class="fas fa-edit text-warning"></i> Additional shipping details can be modified</li>
</ul>
<div class="alert alert-info">
<i class="fas fa-lightbulb"></i>
<strong>Tip:</strong> Save the basic shipping information first, then you can add documents and make further modifications.
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</form>

View File

@ -1,20 +1,22 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.CreateShipmentModel
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.EditShippingModel
@{
// Layout = "_AdminLayout";
ViewBag.PageTitle = "Create Shipment";
NopHtml.SetActiveMenuItemSystemName("Shipments.Create");
ViewBag.PageTitle = "Edit Shipping";
NopHtml.SetActiveMenuItemSystemName("Shippings.Edit");
}
<form asp-action="Create" method="post" enctype="multipart/form-data">
<form asp-action="Edit" method="post">
<input asp-for="Id" type="hidden" />
<div class="content-header clearfix">
<h1 class="float-left">
<i class="fas fa-plus"></i>
Create New Shipment
<i class="fas fa-edit"></i>
Edit Shipping: @Model.LicencePlate - @(Model.ShippingDate.ToString("yyyy-MM-dd"))
</h1>
<div class="float-right">
<button type="submit" name="save" class="btn btn-primary">
<i class="far fa-save"></i>
Save
Update Shipping
</button>
<a asp-action="List" class="btn btn-default">
<i class="fas fa-arrow-left"></i>
@ -30,52 +32,34 @@
<!-- Basic Information -->
<div class="card card-default">
<div class="card-header">
<div class="card-title">Shipment Information</div>
<div class="card-title">Shipping Information</div>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="ShipmentName" />
<nop-label asp-for="ShippingDate" />
</div>
<div class="col-md-9">
<nop-editor asp-for="ShipmentName" required="true" />
<span asp-validation-for="ShipmentName"></span>
<nop-editor asp-for="ShippingDate" required="true" />
<span asp-validation-for="ShippingDate"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="Description" />
<nop-label asp-for="LicencePlate" />
</div>
<div class="col-md-9">
<textarea asp-for="Description" class="form-control" rows="3"></textarea>
<span asp-validation-for="Description"></span>
<nop-editor asp-for="LicencePlate" required="true" />
<span asp-validation-for="LicencePlate"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="ShipmentDate" />
</div>
<div class="col-md-9">
<nop-editor asp-for="ShipmentDate" asp-template="DateTime" required="true" />
<span asp-validation-for="ShipmentDate"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="TrackingNumber" />
</div>
<div class="col-md-9">
<nop-editor asp-for="TrackingNumber" />
<span asp-validation-for="TrackingNumber"></span>
</div>
</div>
</div>
</div>
<!-- File Upload Section -->
<!-- File Upload Section - Only shown when editing existing Shipping -->
<div class="card card-default">
<div class="card-header">
<div class="card-title">
@ -106,9 +90,37 @@
<p id="uploadStatus" class="text-center"></p>
</div>
<!-- Uploaded Files List -->
<div id="uploadedFiles" class="mt-3">
<h5>Uploaded Files:</h5>
<!-- Existing Documents -->
@if (Model.ExistingDocuments != null && Model.ExistingDocuments.Any())
{
<div class="mt-3">
<h5>Existing Documents:</h5>
<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>
}
<!-- Newly Uploaded Files List -->
<div id="uploadedFiles" class="mt-3" style="display: none;">
<h5>Newly Uploaded Files:</h5>
<div id="filesList" class="list-group">
<!-- Uploaded files will appear here -->
</div>
@ -180,6 +192,7 @@
<script>
$(document).ready(function() {
let uploadedFiles = [];
const ShippingId = @Model.Id;
// File input and drop zone elements
const dropZone = document.getElementById('dropZone');
@ -246,6 +259,7 @@
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('ShippingId', ShippingId);
// Show upload progress
showUploadProgress(`Uploading ${file.name}...`);
@ -317,12 +331,10 @@
}
function showError(message) {
// Use nopCommerce's notification system
displayBarNotification(message, 'error', 5000);
}
function showSuccess(message) {
// Use nopCommerce's notification system
displayBarNotification(message, 'success', 3000);
}
@ -336,14 +348,10 @@
$.post('@Url.Action("DeleteUploadedFile")', { filePath: filePath })
.done(function(result) {
if (result.success) {
// Remove from uploaded files array
uploadedFiles = uploadedFiles.filter(f => f.filePath !== filePath);
// Remove from DOM
const fileItem = button.closest('.file-item');
fileItem.remove();
// Hide uploaded files section if no files remain
if (uploadedFiles.length === 0) {
document.getElementById('uploadedFiles').style.display = 'none';
}
@ -358,5 +366,23 @@
});
}
};
window.deleteExistingFile = function(documentId, button) {
if (confirm('Are you sure you want to delete this document?')) {
$.post('@Url.Action("DeleteDocument")', { documentId: documentId })
.done(function(result) {
if (result.success) {
const fileItem = button.closest('.file-item');
fileItem.remove();
showSuccess('Document deleted successfully');
} else {
showError('Failed to delete document: ' + result.message);
}
})
.fail(function() {
showError('Failed to delete document');
});
}
};
});
</script>

View File

@ -0,0 +1,132 @@
@using Newtonsoft.Json
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.ShippingListModel
@{
// Layout = "_AdminLayout";
ViewBag.PageTitle = "Edit Shipping";
NopHtml.SetActiveMenuItemSystemName("Shippings.Edit");
}
<div class="content-header clearfix">
<h1 class="float-left">
@T("Admin.Shipments")
</h1>
<div class="float-right">
<div class="btn-group">
<button type="button" class="btn btn-success">
<i class="fas fa-download"></i>
@T("Admin.Common.Export")
</button>
<button type="button" class="btn btn-success dropdown-toggle dropdown-icon" data-toggle="dropdown" aria-expanded="false">
<span class="sr-only">&nbsp;</span>
</button>
<ul class="dropdown-menu" role="menu">
<li class="dropdown-item">
<button asp-action="ExportXml" type="submit" name="exportxml-all">
<i class="far fa-file-code"></i>
@T("Admin.Common.ExportToXml.All")
</button>
</li>
<li class="dropdown-item">
<button type="button" id="exportxml-selected">
<i class="far fa-file-code"></i>
@T("Admin.Common.ExportToXml.Selected")
</button>
</li>
<li class="dropdown-divider"></li>
<li class="dropdown-item">
<button asp-action="ExportExcel" type="submit" name="exportexcel-all">
<i class="far fa-file-excel"></i>
@T("Admin.Common.ExportToExcel.All")
</button>
</li>
<li class="dropdown-item">
<button type="button" id="exportexcel-selected">
<i class="far fa-file-excel"></i>
@T("Admin.Common.ExportToExcel.Selected")
</button>
</li>
</ul>
</div>
<button type="button" name="importexcel" class="btn bg-olive" data-toggle="modal" data-target="#importexcel-window">
<i class="fas fa-upload"></i>
@T("Admin.Common.Import")
</button>
<div class="btn-group">
<button type="button" class="btn btn-info">
<i class="far fa-file-pdf"></i>
@T("Admin.Orders.PdfInvoices")
</button>
<button type="button" class="btn btn-info dropdown-toggle dropdown-icon" data-toggle="dropdown" aria-expanded="false">
<span class="sr-only">&nbsp;</span>
</button>
<ul class="dropdown-menu" role="menu">
<li class="dropdown-item">
<button asp-action="PdfInvoice" type="submit" name="pdf-invoice-all">
@T("Admin.Orders.PdfInvoices.All")
</button>
</li>
<li class="dropdown-item">
<button type="button" id="pdf-invoice-selected">
@T("Admin.Orders.PdfInvoices.Selected")
</button>
</li>
</ul>
</div>
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.OrderListButtons, additionalData = Model })
</div>
</div>
<section class="content">
<div class="container-fluid">
@{
var list = Model.ShippingList;
}
@(Html.DevExtreme().DataGrid()
.ID("gridContainer")
.ShowBorders(true)
.DataSource(list)
.KeyExpr("Id")
.Columns(c => {
c.Add().DataField("Id");
c.Add().DataField("ShippingDate");
c.Add().DataField("LicencePlate");
c.Add().DataField("IsAllMeasured");
}).MasterDetail(md => {
md.Enabled(true);
md.Template(@<text>
<div class="master-detail-caption"><%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:</div>
@(Html.DevExtreme().DataGrid<FruitBank.Common.Entities.ShippingDocument>()
.ColumnAutoWidth(true)
.ShowBorders(true)
.Columns(columns => {
columns.AddFor(m => m.Id);
columns.AddFor(m => m.Country);
columns.AddFor(m => m.Created);
columns.AddFor(m => m.PartnerId);
columns.Add()
.Caption("Completed")
.DataType(GridColumnDataType.Boolean)
.CalculateCellValue("calculateCellValue");
})
.DataSource(new JS("data.ShippingDocuments"))
)
</text>);
})
)
)
</div>
</section>
<script>
function calculateCellValue(rowData) {
return rowData.Status === "Completed";
}
</script>

View File

@ -0,0 +1,122 @@
@inject INopFileProvider fileProvider;
@inject IWebHelper webHelper
@inject LocalizationSettings localizationSettings
@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment WebHostEnvironment
@using Nop.Core.Domain.Localization
@{
var supportRtl = (await workContext.GetWorkingLanguageAsync()).Rtl && !localizationSettings.IgnoreRtlPropertyForAdminArea;
var culture = CultureInfo.CurrentCulture;
var uiCulture = CultureInfo.CurrentUICulture;
//Code to get check if current cultures scripts are exists. If not, select parent cultures scripts
string GetDefaultCulture()
{
var localePattern = NopCommonDefaults.LocalePatternPath;
var cultureToUse = NopCommonDefaults.DefaultLocalePattern; //Default regionalisation to use
if (fileProvider.DirectoryExists(fileProvider.Combine(WebHostEnvironment.WebRootPath, string.Format(localePattern, culture.Name))))
cultureToUse = culture.Name;
else if (fileProvider.DirectoryExists(fileProvider.Combine(WebHostEnvironment.WebRootPath, string.Format(localePattern, culture.TwoLetterISOLanguageName))))
cultureToUse = culture.TwoLetterISOLanguageName;
return cultureToUse;
}
}
@*Google Font*@
<!link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic" />
@* CSS resources *@
<link rel="stylesheet" href="~/lib_npm/jquery-ui-dist/jquery-ui.min.css"/>
<link rel="stylesheet" href="~/lib_npm/bootstrap-touchspin/jquery.bootstrap-touchspin.min.css"/>
<link rel="stylesheet" href="~/lib_npm/@("@fortawesome")/fontawesome-free/css/all.min.css"/>
<link rel="stylesheet" href="~/lib_npm/datatables.net-bs4/css/dataTables.bootstrap4.min.css"/>
<link rel="stylesheet" href="~/lib_npm/datatables.net-buttons-bs4/css/buttons.bootstrap4.min.css"/>
<link rel="stylesheet" href="~/lib_npm/overlayscrollbars/styles/overlayscrollbars.min.css" />
@if (supportRtl)
{
<link rel="stylesheet" href="~/lib_npm/@("@laylazi")/bootstrap-rtl/css/bootstrap-rtl.min.css"/>
<link rel="stylesheet" href="~/lib/adminLTE/css/adminlte-rtl.min.css"/>
<link rel="stylesheet" href="~/css/admin/styles.rtl.css"/>
}
else
{
<link rel="stylesheet" href="~/lib_npm/admin-lte/css/adminlte.min.css"/>
<link rel="stylesheet" href="~/css/admin/styles.css"/>
}
<link rel="stylesheet" href="~/lib_npm/admin-lte/plugins/select2/css/select2.min.css">
<link href="~/Plugins/Misc.FruitBankPlugin/css/devextreme/dx.light.css" rel="stylesheet" />
@NopHtml.GenerateCssFiles()
<script asp-location="None" src="~/lib_npm/jquery/jquery.min.js"></script>
@* <script src="~/Plugins/Misc.FruitBankPlugin/js/devextreme/jquery.js"></script> *@
<script src="~/lib_npm/jquery-ui-dist/jquery-ui.min.js"></script>
<script src="~/lib_npm/admin-lte/js/adminlte.min.js"></script>
<script src="~/lib_npm/overlayscrollbars/browser/overlayscrollbars.browser.es6.min.js"></script>
<script src="~/lib_npm/bootstrap-touchspin/jquery.bootstrap-touchspin.min.js"></script>
<script src="~/lib_npm/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="~/lib_npm/jquery-validation/jquery.validate.min.js"></script>
<script src="~/lib_npm/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script src="~/lib_npm/admin-lte/plugins/select2/js/select2.full.min.js"></script>
@* cldr scripts (needed for globalize) *@
<script src="~/lib_npm/cldrjs/cldr.js"></script>
<script src="~/lib_npm/cldrjs/cldr/event.js"></script>
<script src="~/lib_npm/cldrjs/cldr/supplemental.js"></script>
@* globalize scripts *@
<script src="~/lib_npm/globalize/globalize.js"></script>
<script src="~/lib_npm/globalize/globalize/number.js"></script>
<script src="~/lib_npm/globalize/globalize/date.js"></script>
<script src="~/lib_npm/globalize/globalize/currency.js"></script>
<script src="~/lib_npm/jquery-migrate/jquery-migrate.min.js"></script>
<script src="~/lib_npm/typeahead.js/typeahead.bundle.min.js"></script>
<script src="~/js/admin.common.js"></script>
<script src="~/js/admin.navigation.js"></script>
<script src="~/js/admin.search.js"></script>
<script src="~/lib_npm/datatables.net/js/dataTables.min.js"></script>
<script src="~/lib_npm/datatables.net-bs4/js/dataTables.bootstrap4.min.js"></script>
<script src="~/lib_npm/moment/min/moment-with-locales.min.js"></script>
<script src="~/lib_npm/datatables.net-buttons/js/dataTables.buttons.min.js"></script>
<script src="~/lib_npm/datatables.net-buttons-bs4/js/buttons.bootstrap4.min.js"></script>
<script asp-location="Footer">
var rootAppPath = '@(Url.Content("~/"))';
var culture = "@GetDefaultCulture()";
//load cldr for current culture
$.when(
$.get({ url: rootAppPath + "lib_npm/cldr-data/supplemental/likelySubtags.json", dataType: "json"}),
$.get({ url: rootAppPath + "lib_npm/cldr-data/main/" + culture + "/numbers.json", dataType: "json"}),
$.get({ url: rootAppPath + "lib_npm/cldr-data/main/" + culture + "/currencies.json", dataType: "json"}),
$.get({ url: rootAppPath + "lib_npm/cldr-data/supplemental/numberingSystems.json", dataType: "json"}),
$.get({ url: rootAppPath + "lib_npm/cldr-data/main/" + culture + "/ca-gregorian.json", dataType: "json"}),
$.get({ url: rootAppPath + "lib_npm/cldr-data/main/" + culture + "/timeZoneNames.json", dataType: "json"}),
$.get({ url: rootAppPath + "lib_npm/cldr-data/supplemental/timeData.json", dataType: "json"}),
$.get({ url: rootAppPath + "lib_npm/cldr-data/supplemental/weekData.json", dataType: "json"}),
).then(function () {
// Normalize $.get results, we only need the JSON, not the request statuses.
return [].slice.apply(arguments, [0]).map(function (result) {
return result[0];
});
}).then(Globalize.load).then(function () {
Globalize.locale(culture);
});
</script>
@NopHtml.GenerateScripts(ResourceLocation.Head)
@NopHtml.GenerateInlineScripts(ResourceLocation.Head)
<script src="~/Plugins/Misc.FruitBankPlugin/js/devextreme/dx.web.js"></script>
<script src="~/Plugins/Misc.FruitBankPlugin/js/devextreme/aspnet/dx.aspnet.mvc.js"></script>
<script src="~/Plugins/Misc.FruitBankPlugin/js/devextreme/aspnet/dx.aspnet.data.js"></script>

View File

@ -0,0 +1,259 @@
@inject IWebHelper webHelper
@inject IDateTimeHelper dateTimeHelper
@inject IPermissionService permissionService
@inject ICustomerService customerService
@inject IEventPublisher eventPublisher
@inject LocalizationSettings localizationSettings
@inject StoreInformationSettings storeInformationSettings
@inject Nop.Services.Localization.ILanguageService languageService
@using Nop.Core.Domain
@using Nop.Core.Domain.Localization
@using Nop.Services.Customers
@using Nop.Services.Helpers
@using Nop.Services.Security
@*
@Html.DevExpress().GetStyleSheets()
@Html.DevExpress().GetScripts() *@
@{
var returnUrl = webHelper.GetRawUrl(Context.Request);
//page title
string adminPageTitle = !string.IsNullOrWhiteSpace(ViewBag.PageTitle) ? ViewBag.PageTitle + " / " : "";
adminPageTitle += T("Admin.PageTitle").Text;
//has "Manage Maintenance" permission?
var canManageMaintenance = await permissionService.AuthorizeAsync(StandardPermission.System.MANAGE_MAINTENANCE);
//avatar
var currentCustomer = await workContext.GetCurrentCustomerAsync();
//event
await eventPublisher.PublishAsync(new PageRenderingEvent(NopHtml));
//info: we specify "Admin" area for actions and widgets here for cases when we use this layout in a plugin that is running in a different area than "admin"
}
<!DOCTYPE html>
<html lang="@CultureInfo.CurrentUICulture.TwoLetterISOLanguageName" dir="@Html.GetUIDirection(localizationSettings.IgnoreRtlPropertyForAdminArea)">
<head>
<title>@adminPageTitle</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
@NopHtml.GenerateHeadCustom()
@* CSS & Script resources *@
@{
await Html.RenderPartialAsync("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/_AdminScripts.cshtml");
}
@*Insert favicon and app icons head code*@
@await Component.InvokeAsync(typeof(FaviconViewComponent))
</head>
<body class="hold-transition sidebar-mini layout-fixed control-sidebar-slide-open">
<div class="throbber">
<div class="curtain">
</div>
<div class="curtain-content">
<div>
<h1 class="throbber-header">@T("Common.Wait")</h1>
<p>
<img src="@Url.Content("~/css/admin/images/throbber-synchronizing.gif")" alt="" />
</p>
</div>
</div>
</div>
<div id="ajaxBusy">
<span>&nbsp;</span>
</div>
<div class="wrapper">
@if (IsSectionDefined("header"))
{
@RenderSection("header")
}
else
{
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.HeaderBefore })
<nav class="main-header navbar navbar-expand-md navbar-dark bg-dark">
<ul class="navbar-nav pl-2 pl-md-0">
<li class="nav-item">
<a class="nav-link" id="nopSideBarPusher" data-widget="pushmenu" href="#"><i class="fa fa-bars"></i></a>
</li>
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.HeaderToggleAfter })
</ul>
<a href="@Url.Content("~/Admin")" class="brand-link navbar-dark">
<img src="~/css/admin/images/logo.png" alt="logo.png" class="brand-image logo d-block d-md-none d-sm-block d-sm-none">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.HeaderNavbarBefore })
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav ml-auto pl-2">
<li class="nav-item">
@await Component.InvokeAsync(typeof(AdminLanguageSelectorViewComponent))
</li>
@if (await customerService.IsRegisteredAsync(currentCustomer))
{
<li class="nav-item">
<a href="#" class="nav-link disabled">@await customerService.GetCustomerFullNameAsync(currentCustomer)</a>
</li>
<li class="nav-item">
<a asp-controller="Customer" asp-action="Logout" asp-area="" class="nav-link">@T("Admin.Header.Logout")</a>
</li>
}
<li class="nav-item">
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.HeaderMiddle })
</li>
<li class="nav-item">
<a asp-controller="Home" asp-action="Index" asp-area="" class="nav-link">@T("Admin.Header.PublicStore")</a>
</li>
@if (canManageMaintenance)
{
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-toggle="dropdown">
<i class="fas fa-gears"></i>
</a>
<ul class="maintenance-menu dropdown-menu dropdown-menu-right" role="menu">
<li class="dropdown-item">
<form asp-controller="Common" asp-action="ClearCache" asp-area="@AreaNames.ADMIN">
<input name="returnurl" type="hidden" value="@returnUrl">
<div class="input-group-append">
<button type="submit" style="width:210px;background-color: Transparent;">
<span>@T("Admin.Header.ClearCache")</span>
</button>
</div>
</form>
</li>
<li class="dropdown-item">
<form asp-controller="Common" asp-action="RestartApplication" asp-area="@AreaNames.ADMIN">
<input name="returnurl" type="hidden" value="@returnUrl">
<button id="restart-application" type="submit" style="width:210px;background-color: Transparent;">
<span>@T("Admin.Header.RestartApplication")</span>
</button>
<script>
$(function() {
$("#restart-application").click(function (e) {
showThrobber('@Html.Raw(JavaScriptEncoder.Default.Encode(T("Admin.Header.RestartApplication.Progress").Text))');
});
});
</script>
</form>
</li>
</ul>
</li>
}
</ul>
</div>
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.HeaderNavbarAfter })
</nav>
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.HeaderAfter })
}
@if (IsSectionDefined("headermenu"))
{
@RenderSection("headermenu")
}
else
{
<aside class="main-sidebar sidebar-dark-primary elevation-4">
<!-- Brand Logo -->
<a href="@Url.Content("~/Admin")" class="brand-link navbar-dark logo-switch">
<img src="~/css/admin/images/logo.png" alt="logo.png" class="brand-image-xl logo-xl">
<img src="~/css/admin/images/logo-mini.png" alt="logo.png" class="brand-image-xs logo-xs">
</a>
<div class="sidebar">
<div class="sidebar-form">
<div id="search-box">
<input type="text" class="form-control admin-search-box typeahead" placeholder="@T("Admin.Menu.Search")">
</div>
</div>
<nav class="mt-2">
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.SearchBoxBefore })
<script>
$(function() {
Admin.Search.init();
});
</script>
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.MenuBefore })
@await Html.PartialAsync("~/Areas/Admin/Views/Shared/Menu.cshtml")
</nav>
</div>
</aside>
}
<div class="content-wrapper">
@await Html.PartialAsync("~/Areas/Admin/Views/Shared/Notifications.cshtml")
<nop-antiforgery-token />
@RenderBody()
</div>
<div class="main-footer">
<div class="container-fluid">
<div class="col-md-12">
<div class="row">
@if (!storeInformationSettings.HidePoweredByNopCommerce)
{
<div class="col-md-4 col-xs-12 text-md-left text-center">
@*Would you like to remove the "Powered by nopCommerce" link in the bottom of the footer?
Please find more info at https://www.nopcommerce.com/nopcommerce-copyright-removal-key*@
Powered by <a href="@(OfficialSite.Main + Utm.OnAdminFooter)" target="_blank">nopCommerce</a>
</div>
}
else
{
<div class="col-md-4 col-xs-12"></div>
}
<div class="col-md-4 col-xs-12 text-center">
@((await dateTimeHelper.ConvertToUserTimeAsync(DateTime.Now)).ToString("f", CultureInfo.CurrentCulture))
</div>
<div class="col-md-4 col-xs-12 text-md-right text-center">
<b>nopCommerce version @NopVersion.FULL_VERSION</b>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
var AdminLTEOptions = {
boxWidgetOptions: {
boxWidgetIcons: {
collapse: 'fa-minus',
open: 'fa-plus'
}
}
};
</script>
@{
//scroll to a selected card (if specified)
var selectedCardName = Html.GetSelectedCardName();
if (!String.IsNullOrEmpty(selectedCardName))
{
<script>
location.hash = '#@(selectedCardName)';
</script>
}
}
<a id="backTop" class="btn btn-back-top bg-teal"></a>
<script>
$(function() {
//enable "back top" arrow
$('#backTop').backTop();
//enable tooltips
$('[data-toggle="tooltip"]').tooltip({ placement: 'bottom' });
});
</script>
@NopHtml.GenerateScripts(ResourceLocation.Footer)
@NopHtml.GenerateInlineScripts(ResourceLocation.Footer)
</body>
</html>

View File

@ -70,4 +70,8 @@
@using Nop.Web.Framework.Mvc.Routing
@using Nop.Web.Framework.Security.Captcha
@using Nop.Web.Framework.Security.Honeypot
@using Nop.Web.Framework.Themes
@using Nop.Web.Framework.Themes
@using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin
@* @using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components *@
@using DevExtreme.AspNet.Mvc

View File

@ -1,3 +1,4 @@
@{
Layout = "_AdminLayout";
//Layout = "_AdminLayout";
Layout = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/_FruitBankAdminLayout.cshtml";
}

View File

@ -258,4 +258,10 @@ public class FruitBankDbContext : MgDbContextBase, IPartnerDbSet<PartnerDbTable>
//Logger.Error($"Product updatedRowsCount != 1; id: {product.Id}");
//return await Task.FromResult(false);
}
public async Task<List<ShippingDocument>> GetShippingDocumentsByShippingIdAsync(int shippingId)
{
var list = await ShippingDocuments.GetAll(true).Where(sd => sd.ShippingId == shippingId).ToListAsync();
return list;
}
}

View File

@ -0,0 +1,48 @@
//using FruitBank.Common.Entities;
//using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
//namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Factories
//{
// /// <summary>
// /// Represents the shipping model factory
// /// </summary>
// public interface IFruitBankShippingModelFactory
// {
// /// <summary>
// /// Prepare Shipping search model
// /// </summary>
// /// <param name="searchModel">Shipping search model</param>
// /// <returns>Shipping search model</returns>
// Task<ShippingSearchModel> PrepareShippingSearchModelAsync(ShippingSearchModel searchModel);
// /// <summary>
// /// Prepare paged Shipping list model
// /// </summary>
// /// <param name="searchModel">Shipping search model</param>
// /// <returns>Shipping list model</returns>
// Task<ShippingListModel> PrepareShippingListModelAsync(ShippingSearchModel searchModel);
// /// <summary>
// /// Prepare Shipping model for display
// /// </summary>
// /// <param name="model">Shipping model</param>
// /// <param name="shipping">Shipping entity</param>
// /// <returns>Shipping model</returns>
// Task<ShippingModel> PrepareShippingModelAsync(ShippingModel model, Shipping shipping);
// ///// <summary>
// ///// Prepare create Shipping model
// ///// </summary>
// ///// <returns>Create Shipping model</returns>
// //Task<CreateShippingModel> PrepareCreateShippingModelAsync();
// ///// <summary>
// ///// Prepare edit Shipping model
// ///// </summary>
// ///// <param name="model">Edit Shipping model</param>
// ///// <param name="shipping">Shipping entity</param>
// ///// <returns>Edit Shipping model</returns>
// //Task<EditShippingModel> PrepareEditShippingModelAsync(EditShippingModel model, Shipping shipping);
// }
//}

View File

@ -0,0 +1,189 @@
//using FruitBank.Common.Entities;
//using Nop.Core;
//using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
//using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
//using Nop.Web.Areas.Admin.Models.Orders;
//using Nop.Web.Framework.Models;
//using Nop.Web.Framework.Models.Extensions;
//namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Factories
//{
// /// <summary>
// /// Represents the shipment model factory implementation
// /// </summary>
// public partial class ShippingModelFactory : IFruitBankShippingModelFactory
// {
// #region Fields
// // TODO: Add your services here
// // private readonly IShippingService _shippingService;
// // private readonly IPartnerService _partnerService;
// // private readonly IDocumentService _documentService;
// private readonly ShippingDbTable shippingDbTable;
// #endregion
// #region Ctor
// public ShippingModelFactory(
// // TODO: Add your service dependencies
// )
// {
// // TODO: Initialize services
// }
// #endregion
// #region Methods
// /// <summary>
// /// Prepare shipment search model
// /// </summary>
// /// <param name="searchModel">Shipment search model</param>
// /// <returns>Shipment search model</returns>
// public virtual Task<ShippingSearchModel> PrepareShippingSearchModelAsync(ShippingSearchModel searchModel)
// {
// ArgumentNullException.ThrowIfNull(searchModel);
// // Set default values
// // TODO: Add any additional search model preparation here
// // For example, populate partner dropdown lists, etc.
// searchModel.SetGridPageSize();
// searchModel.Start = 1;
// searchModel.Length = 10;
// searchModel.SearchIsAllMeasured = 0; // All
// searchModel.SearchLicencePlate = string.Empty;
// searchModel.SearchPartnerId = 0;
// searchModel.SearchShippingDateFrom = null;
// searchModel.SearchShippingDateTo = null;
// return Task.FromResult(searchModel);
// }
// /// <summary>
// /// Prepare paged shipment list model
// /// </summary>
// /// <param name="searchModel">Shipment search model</param>
// /// <returns>Shipment list model</returns>
// public virtual async Task<ShippingListModel> PrepareShippingListModelAsync(ShippingSearchModel searchModel)
// {
// ArgumentNullException.ThrowIfNull(searchModel);
// // TODO: Get shipments from your service
// // var shipments = await _shippingService.GetShipmentsAsync(
// // dateFrom: searchModel.SearchShipmentDateFrom,
// // dateTo: searchModel.SearchShipmentDateTo,
// // licencePlate: searchModel.SearchLicencePlate,
// // partnerId: searchModel.SearchPartnerId > 0 ? searchModel.SearchPartnerId : null,
// // isAllMeasured: searchModel.SearchIsAllMeasured == 1 ? true :
// // searchModel.SearchIsAllMeasured == 2 ? false : null,
// // pageIndex: searchModel.Page - 1,
// // pageSize: searchModel.PageSize
// // );
// // Mock data for now - replace with actual service call
// var shipments = shippingDbTable.GetAll().ToList();
// // Prepare list model
// var model = await ModelExtensions.PrepareToGridAsync<ShippingListModel, ShippingModel, Shipping>(
// new ShippingListModel(),
// searchModel,
// shipments.ToPagedList(searchModel),
// () => shipments.SelectAwait(async shipment => new ShippingModel
// {
// Id = shipment.Id,
// PartnerId = shipment.PartnerId,
// ShippingDate = shipment.ShippingDate,
// LicencePlate = shipment.LicencePlate,
// IsAllMeasured = shipment.IsAllMeasured,
// DocumentCount = shipment.ShippingDocuments?.Count ?? 0,
// Created = shipment.Created,
// Modified = shipment.Modified
// })
// );
// return model;
// }
// /// <summary>
// /// Prepare shipment model
// /// </summary>
// /// <param name="model">Shipment model</param>
// /// <param name="shipment">Shipment</param>
// /// <returns>Shipment model</returns>
// public virtual async Task<ShippingModel> PrepareShippingModelAsync(ShippingModel model, Shipping shipment)
// {
// if (shipment != null)
// {
// // Fill the model properties if the model is null
// model ??= new ShippingModel
// {
// Id = shipment.Id,
// PartnerId = shipment.PartnerId,
// ShippingDate = shipment.ShippingDate,
// LicencePlate = shipment.LicencePlate,
// IsAllMeasured = shipment.IsAllMeasured,
// Created = shipment.Created,
// Modified = shipment.Modified
// };
// // TODO: Get partner name from your service
// // model.PartnerName = await _partnerService.GetPartnerNameAsync(shipment.PartnerId);
// model.PartnerName = $"Partner {shipment.PartnerId}"; // Mock for now
// // TODO: Get document count from your service
// // model.DocumentCount = await _documentService.GetDocumentCountByShipmentIdAsync(shipment.Id);
// model.DocumentCount = shipment.ShippingDocuments?.Count ?? 0;
// }
// return model;
// }
// #endregion
// #region Utilities
// // Mock data method - remove when you have real services
// private async Task<IPagedList<Shipping>> GetMockShipmentsAsync(ShippingSearchModel searchModel)
// {
// await Task.Delay(1); // Simulate async operation
// var mockData = new List<Shipping>();
// for (int i = 1; i <= 50; i++)
// {
// mockData.Add(new Shipping
// {
// Id = i,
// PartnerId = (i % 5) + 1,
// ShippingDate = DateTime.Now.AddDays(-i),
// LicencePlate = $"ABC-{i:D3}",
// IsAllMeasured = i % 3 == 0,
// Created = DateTime.Now.AddDays(-i),
// Modified = DateTime.Now.AddDays(-i / 2),
// ShippingDocuments = new List<ShippingDocument>()
// });
// }
// // Apply search filters
// if (searchModel.SearchShippingDateFrom.HasValue)
// mockData = mockData.Where(s => s.ShippingDate >= searchModel.SearchShippingDateFrom.Value).ToList();
// if (searchModel.SearchShippingDateTo.HasValue)
// mockData = mockData.Where(s => s.ShippingDate <= searchModel.SearchShippingDateTo.Value).ToList();
// if (!string.IsNullOrEmpty(searchModel.SearchLicencePlate))
// mockData = mockData.Where(s => s.LicencePlate.Contains(searchModel.SearchLicencePlate, StringComparison.OrdinalIgnoreCase)).ToList();
// if (searchModel.SearchIsAllMeasured == 1)
// mockData = mockData.Where(s => s.IsAllMeasured).ToList();
// else if (searchModel.SearchIsAllMeasured == 2)
// mockData = mockData.Where(s => !s.IsAllMeasured).ToList();
// return new PagedList<Shipping>(mockData, searchModel.Page - 1, searchModel.PageSize);
// }
// #endregion
// }
//}

View File

@ -62,7 +62,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
//TODO: Add unique index to GenericAttribute[EntityId, KeyGroup, Key]???!? ÁTGONDOLNI, mert lehet esetleg lista is a visszaadott, de a nopcommerce kódban felülírja ha azonos key-el vannak! - J.
// Default settings
var settings = new OpenAiSettings
var settings = new FruitBankSettings
{
ApiKey = string.Empty
};
@ -75,7 +75,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
// --- UNINSTALL ---
public override async Task UninstallAsync()
{
await _settingService.DeleteSettingAsync<OpenAiSettings>();
await _settingService.DeleteSettingAsync<FruitBankSettings>();
await base.UninstallAsync();
}

View File

@ -0,0 +1,44 @@
using global::Nop.Core.Configuration;
namespace Nop.Plugin.Misc.FruitBankPlugin
{
public class FruitBankSettings : ISettings
{
/// <summary>
/// Gets or sets the AI API key
/// </summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the AI API model name
/// </summary>
public string ModelName { get; set; } = "gpt-3.5-turbo";
/// <summary>
/// Gets or sets a value indicating whether the AI plugin is enabled
/// </summary>
public bool IsEnabled { get; set; } = false;
/// <summary>
/// Gets or sets the API base URL (useful for different AI providers)
/// </summary>
public string ApiBaseUrl { get; set; } = "https://api.openai.com/v1";
/// <summary>
/// Gets or sets the maximum number of tokens for AI responses
/// </summary>
public int MaxTokens { get; set; } = 1000;
/// <summary>
/// Gets or sets the temperature for AI responses (0.0 to 1.0)
/// </summary>
public decimal Temperature { get; set; } = 0.7m;
/// <summary>
/// Gets or sets the timeout for API requests in seconds
/// </summary>
public int RequestTimeoutSeconds { get; set; } = 30;
}
}

View File

@ -1,6 +1,8 @@
//using AyCode.Core.Loggers;
using AyCode.Core.Loggers;
using DevExpress.AspNetCore;
using FruitBank.Common;
using FruitBank.Common.Interfaces;
using FruitBank.Common.Server.Services.Loggers;
@ -72,6 +74,9 @@ public class PluginNopStartup : INopStartup
services.AddScoped<OrderSearchModel, OrderSearchModelExtended>();
services.AddScoped<IOrderModelFactory, CustomOrderModelFactory>();
services.AddScoped<IGenericAttributeService, GenericAttributeService>();
services.AddScoped<CerebrasAPIService>();
//services.AddScoped<IAIAPIService, OpenAIApiService>();
services.AddScoped<AICalculationService>();
services.AddControllersWithViews(options =>
{
options.Filters.AddService<PendingMeasurementCheckoutFilter>();
@ -93,7 +98,7 @@ public class PluginNopStartup : INopStartup
{
endpoints.MapHub<DevAdminSignalRHub>(fruitBankHubEndPoint);
});
});
});
var loggrHubEndPoint = $"/{FruitBankConstClient.LoggerHubName}";

View File

@ -50,9 +50,15 @@ public class RouteProvider : IRouteProvider
);
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Shipment.List",
pattern: "Admin/Shipment/List",
defaults: new { controller = "Shipment", action = "List", area = AreaNames.ADMIN }
name: "Plugin.FruitBank.Admin.Shipping.List",
pattern: "Admin/Shipping/List",
defaults: new { controller = "Shipping", action = "List", area = AreaNames.ADMIN }
);
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Shipping.ShippingList",
pattern: "Admin/Shipping/ShippingList",
defaults: new { controller = "Shipping", action = "ShippingList", area = AreaNames.ADMIN }
);
endpointRouteBuilder.MapControllerRoute(
@ -62,14 +68,19 @@ public class RouteProvider : IRouteProvider
);
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Shipment.Create",
pattern: "Admin/Shipment/Create",
defaults: new { controller = "Shipment", action = "Create", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.Admin.Shipping.Create",
pattern: "Admin/Shipping/Create",
defaults: new { controller = "Shipping", action = "Create", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Shipment.UploadFile",
pattern: "Admin/Shipment/UploadFile",
defaults: new { controller = "Shipment", action = "UploadFile", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.Admin.Shipping.Edit",
pattern: "Admin/Shipping/Edit",
defaults: new { controller = "Shipping", action = "Edit", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Shipping.UploadFile",
pattern: "Admin/Shipping/UploadFile",
defaults: new { controller = "Shipping", action = "UploadFile", area = AreaNames.ADMIN });
}
/// <summary>

View File

@ -0,0 +1,16 @@
using System.Text.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Nop.Plugin.Misc.FruitBankPlugin.Models
{
public class AIChatMessage
{
[JsonProperty("role")]
public string Role { get; set; } = string.Empty;
[JsonProperty("content")]
public string Content { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,21 @@
using System.Text.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Nop.Plugin.Misc.FruitBankPlugin.Models
{
public class AIChatRequestBase
{
[JsonProperty("model")]
public string Model { get; set; } = "gpt-4o-mini";
[JsonProperty("temperature")]
public double Temperature { get; set; } = 0.2;
[JsonProperty("messages")]
public AIChatMessage[] Messages { get; set; } = Array.Empty<AIChatMessage>();
[JsonProperty("stream")]
public bool Stream { get; set; } = true;
}
}

View File

@ -0,0 +1,14 @@
using System.Text.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Nop.Plugin.Misc.FruitBankPlugin.Models
{
public class CerebrasAIChatRequest : AIChatRequestBase
{
//If any specific properties are needed for this derived class, add them here. - A.
}
}

View File

@ -0,0 +1,15 @@
using System.Text.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Nop.Plugin.Misc.FruitBankPlugin.Models
{
public class OpenAIGpt4MiniAIChatRequest : AIChatRequestBase
{
//If any specific properties are needed for this derived class, add them here. - A.
//Needed only for gpt-5
}
}

View File

@ -0,0 +1,18 @@
using System.Text.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Nop.Plugin.Misc.FruitBankPlugin.Models
{
public class OpenAIGpt5AIChatRequest : AIChatRequestBase
{
//If any specific properties are needed for this derived class, add them here. - A.
//Needed only for gpt-5
[JsonProperty("reasoning_effort")]
public string ReasoningEffort { get; set; } = "minimal";
[JsonProperty("verbosity")]
public string Verbosity { get; set; } = "high";
}
}

View File

@ -1,15 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputPath>$(SolutionDir)\Presentation\Nop.Web\Plugins\Misc.FruitBankPlugin</OutputPath>
<OutDir>$(OutputPath)</OutDir>
<!--Set this parameter to true to get the dlls copied from the NuGet cache to the output of your project.
You need to set this parameter to true if your plugin has a nuget package
to ensure that the dlls copied from the NuGet cache to the output of your project-->
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<ImplicitUsings>enable</ImplicitUsings>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
</PropertyGroup>
<ItemGroup>
@ -19,6 +15,22 @@
<None Remove="Areas\Admin\Views\_ViewImports.cshtml" />
</ItemGroup>
<ItemGroup>
<!-- DevExpress MVC Controls -->
<PackageReference Include="DevExpress.AspNetCore.Common" Version="25.1.3" />
<PackageReference Include="DevExtreme.AspNet.Core" Version="25.1.3" />
<PackageReference Include="DevExtreme.AspNet.Data" Version="5.1.0" />
<!-- Your existing packages -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="PdfPig" Version="0.1.11" />
<PackageReference Include="SendGrid" Version="9.29.3" />
</ItemGroup>
<ItemGroup>
<Content Include="Areas\Admin\Views\Index.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
@ -30,8 +42,13 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Areas\Admin\Views\_AdminScripts.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="logo.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -40,14 +57,8 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Areas\Admin\Views\_ViewImports.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Areas\Admin\Views\_ViewImports.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Views\_ViewImports.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
@ -59,22 +70,13 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Areas\Admin\Components\" />
<Folder Include="Areas\Admin\Extensions\" />
<Folder Include="Areas\Admin\Factories\" />
<Folder Include="Areas\Admin\Validators\" />
<Folder Include="Domains\Entities\" />
<Folder Include="Extensions\" />
<Folder Include="Validators\" />
<Folder Include="Views\Admin\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="SendGrid" Version="9.29.3" />
<Folder Include="Views\Admin\" />
</ItemGroup>
<ItemGroup>
@ -144,6 +146,9 @@
</ItemGroup>
<ItemGroup>
<None Update="Areas\Admin\Components\_WelcomeMessage.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Configure\Configure.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
@ -156,12 +161,432 @@
<None Update="Areas\Admin\Views\Order\List.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Shipment\Create.cshtml">
<None Update="Areas\Admin\Views\Shipping\Edit.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Shipment\List.cshtml">
<None Update="Areas\Admin\Views\Shipping\Create.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Shipping\List.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\_FruitBankAdminLayout.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\bootstrap.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\bootstrap.min.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx-diagram.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx-diagram.min.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx-gantt.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx-gantt.min.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.carmine.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.carmine.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.contrast.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.contrast.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.dark.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.dark.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.darkmoon.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.darkmoon.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.darkviolet.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.darkviolet.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.fluent.blue.dark.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.fluent.blue.dark.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.fluent.blue.light.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.fluent.blue.light.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.fluent.saas.dark.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.fluent.saas.dark.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.fluent.saas.light.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.fluent.saas.light.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.greenmist.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.greenmist.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.light.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.light.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.blue.dark.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.blue.dark.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.blue.light.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.blue.light.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.lime.dark.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.lime.dark.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.lime.light.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.lime.light.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.orange.dark.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.orange.dark.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.orange.light.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.orange.light.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.purple.dark.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.purple.dark.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.purple.light.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.purple.light.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.teal.dark.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.teal.dark.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.teal.light.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.material.teal.light.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.softblue.compact.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\dx.softblue.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-300.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-300.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-300.woff2">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-400.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-400.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-400.woff2">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-500.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-500.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-500.woff2">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-700.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-700.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\fonts\Roboto-700.woff2">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxicons.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxicons.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxicons.woff2">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxiconsfluent.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxiconsfluent.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxiconsfluent.woff2">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxiconsmaterial.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxiconsmaterial.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="css\devextreme\icons\dxiconsmaterial.woff2">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\aspnet\dx.aspnet.data.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\aspnet\dx.aspnet.mvc.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\bootstrap.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\bootstrap.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx-diagram.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx-diagram.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx-gantt.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx-gantt.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx-quill.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx-quill.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx.all.debug.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx.all.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx.viz.debug.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx.viz.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx.web.debug.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\dx.web.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\exceljs.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\exceljs.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\FileSaver.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\FileSaver.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\jquery.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\jquery.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\jspdf.plugin.autotable.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\jspdf.plugin.autotable.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\jspdf.umd.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\jspdf.umd.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\jszip.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\jszip.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.ar.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.bg.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.ca.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.cs.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.da.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.de.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.el.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.en.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.es.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.fa.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.fi.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.fr.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.hu.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.it.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.ja.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.lt.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.lv.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.nb.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.nl.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.pl.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.pt.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.ro.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.ru.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.sl.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.sv.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.tr.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.uk.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.vi.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.zh-tw.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\localization\dx.messages.zh.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-data\africa.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-data\canada.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-data\eurasia.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-data\europe.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-data\usa.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-data\world.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-utils\dx.vectormaputils.debug.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-utils\dx.vectormaputils.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="js\devextreme\vectormap-utils\dx.vectormaputils.node.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Views\Checkout\PendingMeasurementWarning.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
@ -172,10 +597,13 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="$(OutDir)\System.ServiceModel.Primitives.dll" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<!-- This target execute after "Build" target -->
<Target Name="NopTarget" AfterTargets="Build">
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
</Target>
</Project>
</Project>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!-- Alternatively, use the DevExpress NuGet Feed: https://nuget.devexpress.com -->
<add key="devextreme-controls-netcore" value="C:\Program Files\DevExpress 25.1\DevExtreme\System\DevExtreme\Bin\AspNetCore" />
</packageSources>
</configuration>

View File

@ -1,10 +0,0 @@
using global::Nop.Core.Configuration;
namespace Nop.Plugin.Misc.FruitBankPlugin
{
public class OpenAiSettings : ISettings
{
public string ApiKey { get; set; }
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,194 @@
using System.Text.Json;
using System.Text;
using Microsoft.Extensions.Configuration;
using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Plugin.Misc.FruitBankPlugin.Services;
using Nop.Services.Configuration;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
public class CerebrasAPIService : IAIAPIService
{
private readonly ISettingService _settingService;
private readonly FruitBankSettings _fruitBankSettings;
private readonly HttpClient _httpClient;
private static Action<string, string>? _callback;
private static Action<string>? _onComplete;
private static Action<string, string>? _onError;
private const string CerebrasEndpoint = "https://api.cerebras.ai/v1/chat/completions";
public CerebrasAPIService(ISettingService settingService, HttpClient httpClient)
{
_settingService = settingService;
_fruitBankSettings = _settingService.LoadSetting<FruitBankSettings>();
_httpClient = httpClient;
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {GetApiKey()}");
}
public string GetApiKey() =>
_fruitBankSettings.ApiKey;
//_configuration?.GetSection("Cerebras")?.GetValue<string>("ApiKey") ?? string.Empty;
public string GetModelName() =>
_fruitBankSettings.ModelName;
//_configuration?.GetSection("Cerebras")?.GetValue<string>("Model") ?? string.Empty;
public void RegisterCallback(Action<string, string> callback, Action<string> onCompleteCallback, Action<string, string> onErrorCallback)
{
_callback = callback;
_onComplete = onCompleteCallback;
_onError = onErrorCallback;
}
public async Task<string> GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null)
{
string modelName = GetModelName();
var requestBody = new CerebrasAIChatRequest
{
Model = modelName,
Temperature = 0.2,
Messages = assistantMessage == null || assistantMessage == string.Empty
? new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "user", Content = userMessage }
}
: new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "assistant", Content = assistantMessage },
new AIChatMessage { Role = "user", Content = userMessage }
},
Stream = false
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync(CerebrasEndpoint, requestContent);
response.EnsureSuccessStatusCode();
using var responseStream = await response.Content.ReadAsStreamAsync();
using var document = await JsonDocument.ParseAsync(responseStream);
var inputTokens = document.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32();
var outputTokens = document.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32();
var sum = inputTokens + outputTokens;
Console.WriteLine($"USAGE STATS - Tokens: {inputTokens.ToString()} + {outputTokens.ToString()} = {sum.ToString()}");
return document.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString() ?? "No response";
}
public async Task<string> GetStreamedResponseAsync(string sessionId, string systemMessage, string userMessage, string? assistantMessage = null)
{
string modelName = GetModelName();
var requestBody = new CerebrasAIChatRequest
{
Model = modelName,
Temperature = 0.2,
Messages = assistantMessage == null
? new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "user", Content = userMessage }
}
: new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "assistant", Content = assistantMessage },
new AIChatMessage { Role = "user", Content = userMessage }
},
Stream = true
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, CerebrasEndpoint)
{
Content = requestContent
};
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var stringBuilder = new StringBuilder();
using var responseStream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(responseStream);
try
{
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue;
var jsonResponse = line.Substring(6);
// ✅ Detect explicit end of stream
if (jsonResponse == "[DONE]")
{
_onComplete?.Invoke(sessionId); // Optional: notify stream end
break;
}
try
{
using var jsonDoc = JsonDocument.Parse(jsonResponse);
if (jsonDoc.RootElement.TryGetProperty("choices", out var choices) &&
choices[0].TryGetProperty("delta", out var delta) &&
delta.TryGetProperty("content", out var contentElement))
{
var content = contentElement.GetString();
if (!string.IsNullOrEmpty(content))
{
stringBuilder.Append(content);
_callback?.Invoke(sessionId, stringBuilder.ToString());
}
}
}
catch (JsonException ex)
{
_onError?.Invoke(sessionId, $"Malformed JSON: {ex.Message}");
break; // Optionally stop stream
}
}
// ✅ Check for unexpected end (in case no [DONE])
if (reader.EndOfStream && !stringBuilder.ToString().EndsWith("[DONE]"))
{
_onError?.Invoke(sessionId, "Unexpected end of stream");
}
}
catch (Exception ex)
{
_onError?.Invoke(sessionId, $"Exception: {ex.Message}");
}
return stringBuilder.ToString();
}
}
}

View File

@ -124,38 +124,47 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
var rootNode = eventMessage.RootMenuItem;
var shipmentsListMenuItem = new AdminMenuItem
var ShippingsListMenuItem = new AdminMenuItem
{
Visible = true,
SystemName = "FruitBank",
Title = await _localizationService.GetResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList"), // You can localize this with await _localizationService.GetResourceAsync("...")
Title = await _localizationService.GetResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShippingsList"), // You can localize this with await _localizationService.GetResourceAsync("...")
IconClass = "fas fa-shipping-fast",
Url = _adminMenu.GetMenuItemUrl("Shipment", "List")
Url = _adminMenu.GetMenuItemUrl("Shipping", "List")
};
var createShipmentMenuItem = new AdminMenuItem
var createShippingMenuItem = new AdminMenuItem
{
Visible = true,
SystemName = "Shipments.Create",
Title = "Create Shipment",
SystemName = "Shippings.Create",
Title = "Create Shipping",
IconClass = "far fa-circle",
Url = _adminMenu.GetMenuItemUrl("Shipment", "Create")
Url = _adminMenu.GetMenuItemUrl("Shipping", "Create")
};
var editShippingMenuItem = new AdminMenuItem
{
Visible = true,
SystemName = "Shippings.Edit",
Title = "Edit Shipping",
IconClass = "far fa-circle",
Url = _adminMenu.GetMenuItemUrl("Shipping", "Edit")
};
// Create a new top-level menu item
var shipmentsMenuItem = new AdminMenuItem
var ShippingsMenuItem = new AdminMenuItem
{
Visible = true,
SystemName = "FruitBank",
Title = await _localizationService.GetResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.Shipments"), // You can localize this with await _localizationService.GetResourceAsync("...")
Title = await _localizationService.GetResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.Shippings"), // You can localize this with await _localizationService.GetResourceAsync("...")
IconClass = "fas fa-shipping-fast",
//Url = _adminMenu.GetMenuItemUrl("Shipment", "List")
ChildNodes = new[] { shipmentsListMenuItem, createShipmentMenuItem }
//Url = _adminMenu.GetMenuItemUrl("Shipping", "List")
ChildNodes = new[] { ShippingsListMenuItem, createShippingMenuItem, editShippingMenuItem }
};
var shipmentConfigurationItem = rootNode;
shipmentConfigurationItem.ChildNodes.Insert(2, shipmentsMenuItem);
var ShippingConfigurationItem = rootNode;
ShippingConfigurationItem.ChildNodes.Insert(2, ShippingsMenuItem);
var invoiceListMenuItem = new AdminMenuItem
@ -174,7 +183,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
SystemName = "FruitBank",
Title = await _localizationService.GetResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.Invoices"), // You can localize this with await _localizationService.GetResourceAsync("...")
IconClass = "fas fa-file-invoice",
//Url = _adminMenu.GetMenuItemUrl("Shipment", "List")
//Url = _adminMenu.GetMenuItemUrl("Shipping", "List")
ChildNodes = new[] { invoiceListMenuItem }
};

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
internal interface IAIAPIService
{
Task<string> GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null);
Task<string> GetStreamedResponseAsync(string sessionId, string systemMessage, string userMessage, string? assistantMessage = null);
string GetApiKey();
string GetModelName();
}
}

View File

@ -0,0 +1,351 @@
using Microsoft.Extensions.Configuration;
using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Plugin.Misc.FruitBankPlugin.Services;
using Nop.Services.Configuration;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
public class OpenAIApiService : IAIAPIService
{
private readonly ISettingService _settingService;
private readonly FruitBankSettings _fruitBankSettings;
private readonly HttpClient _httpClient;
private static Action<string, string>? _callback;
private static Action<string>? _onComplete;
private static Action<string, string>? _onError;
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 OpenAiFileEndpoint = "https://api.openai.com/v1/files";
public OpenAIApiService(ISettingService settingService, HttpClient httpClient)
{
_settingService = settingService;
_fruitBankSettings = _settingService.LoadSetting<FruitBankSettings>();
_httpClient = httpClient;
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {GetApiKey()}");
}
public string GetApiKey() => _fruitBankSettings.ApiKey;
public string GetModelName() => _fruitBankSettings.ModelName;
public void RegisterCallback(Action<string, string> callback, Action<string> onCompleteCallback, Action<string, string> onErrorCallback)
{
_callback = callback;
_onComplete = onCompleteCallback;
_onError = onErrorCallback;
}
#region === CHAT (TEXT INPUT) ===
public async Task<string> GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null)
{
string modelName = GetModelName();
StringContent requestContent = new("");
if (modelName == "gpt-4.1-mini" || modelName == "gpt-4o-mini" || modelName == "gpt-4.1-nano" || modelName == "gpt-5-nano")
{
var requestBody = new OpenAIGpt4MiniAIChatRequest
{
Model = modelName,
Temperature = 0.2,
Messages = assistantMessage == null || assistantMessage == string.Empty
? new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "user", Content = userMessage }
}
: new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "assistant", Content = assistantMessage },
new AIChatMessage { Role = "user", Content = userMessage }
},
Stream = false
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
}
else
{
var requestBody = new OpenAIGpt5AIChatRequest
{
Model = modelName,
Temperature = 1,
Messages = assistantMessage == null || assistantMessage == string.Empty
? new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "user", Content = userMessage }
}
: new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "assistant", Content = assistantMessage },
new AIChatMessage { Role = "user", Content = userMessage }
},
ReasoningEffort = "minimal",
Verbosity = "high",
Stream = false
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
}
using var response = await _httpClient.PostAsync(OpenAiEndpoint, requestContent);
response.EnsureSuccessStatusCode();
using var responseStream = await response.Content.ReadAsStreamAsync();
using var document = await JsonDocument.ParseAsync(responseStream);
var inputTokens = document.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32();
var outputTokens = document.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32();
Console.WriteLine($"USAGE STATS - Tokens: {inputTokens} + {outputTokens} = {inputTokens + outputTokens}");
return document.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString() ?? "No response";
}
#endregion
#region === CHAT (STREAMING) ===
public async Task<string> GetStreamedResponseAsync(string sessionId, string systemMessage, string userMessage, string? assistantMessage = null)
{
string modelName = GetModelName();
StringContent requestContent = new("");
if (modelName == "gpt-4.1-mini" || modelName == "gpt-4o-mini" || modelName == "gpt-4.1-nano" || modelName == "gpt-5-nano")
{
var requestBody = new OpenAIGpt4MiniAIChatRequest
{
Model = modelName,
Temperature = 0.2,
Messages = assistantMessage == null || assistantMessage == string.Empty
? new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "user", Content = userMessage }
}
: new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "assistant", Content = assistantMessage },
new AIChatMessage { Role = "user", Content = userMessage }
},
Stream = false
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
}
else
{
var requestBody = new OpenAIGpt5AIChatRequest
{
Model = modelName,
Temperature = 1,
Messages = assistantMessage == null || assistantMessage == string.Empty
? new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "user", Content = userMessage }
}
: new[]
{
new AIChatMessage { Role = "system", Content = systemMessage },
new AIChatMessage { Role = "assistant", Content = assistantMessage },
new AIChatMessage { Role = "user", Content = userMessage }
},
Stream = false
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
}
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, OpenAiEndpoint)
{
Content = requestContent
};
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var stringBuilder = new StringBuilder();
using var responseStream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(responseStream);
try
{
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue;
var jsonResponse = line.Substring(6);
if (jsonResponse == "[DONE]")
{
_onComplete?.Invoke(sessionId);
break;
}
try
{
using var jsonDoc = JsonDocument.Parse(jsonResponse);
if (jsonDoc.RootElement.TryGetProperty("choices", out var choices) &&
choices[0].TryGetProperty("delta", out var delta) &&
delta.TryGetProperty("content", out var contentElement))
{
var content = contentElement.GetString();
if (!string.IsNullOrEmpty(content))
{
stringBuilder.Append(content);
_callback?.Invoke(sessionId, stringBuilder.ToString());
}
}
}
catch (JsonException ex)
{
_onError?.Invoke(sessionId, $"Malformed JSON: {ex.Message}");
break;
}
}
if (reader.EndOfStream && !stringBuilder.ToString().EndsWith("[DONE]"))
{
_onError?.Invoke(sessionId, "Unexpected end of stream");
}
}
catch (Exception ex)
{
_onError?.Invoke(sessionId, $"Exception: {ex.Message}");
}
return stringBuilder.ToString();
}
#endregion
#region === IMAGE GENERATION ===
public async Task<string?> GenerateImageAsync(string prompt)
{
var request = new HttpRequestMessage(HttpMethod.Post, OpenAiImageEndpoint);
var requestBody = new
{
model = "gpt-image-1",
prompt = prompt,
n = 1,
size = "1024x1024"
};
request.Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Image generation failed: {error}");
return null;
}
using var content = await response.Content.ReadAsStreamAsync();
var json = await JsonDocument.ParseAsync(content);
var base64Image = json.RootElement
.GetProperty("data")[0]
.GetProperty("b64_json")
.GetString();
return $"data:image/png;base64,{base64Image}";
}
#endregion
#region === PDF ANALYSIS (NEW) ===
public async Task<string?> AnalyzePdfAsync(string filePath, string userPrompt)
{
// Step 1: Upload PDF
using var form = new MultipartFormDataContent();
using var fileStream = File.OpenRead(filePath);
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(fileContent, "file", Path.GetFileName(filePath));
form.Add(new StringContent("assistants"), "purpose");
var uploadResponse = await _httpClient.PostAsync(OpenAiFileEndpoint, form);
if (!uploadResponse.IsSuccessStatusCode)
{
var error = await uploadResponse.Content.ReadAsStringAsync();
throw new Exception($"File upload failed: {error}");
}
using var uploadJson = await JsonDocument.ParseAsync(await uploadResponse.Content.ReadAsStreamAsync());
var fileId = uploadJson.RootElement.GetProperty("id").GetString();
// Step 2: Ask model with file reference
var requestBody = new
{
model = "gpt-4.1", // must support file_search
messages = new[]
{
new { role = "system", content = "You are an assistant that analyzes uploaded PDF files." },
new { role = "user", content = userPrompt }
},
tools = new[]
{
new { type = "file_search" }
},
tool_resources = new
{
file_search = new
{
vector_store_ids = new string[] { fileId! }
}
}
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
var chatResponse = await _httpClient.PostAsync(OpenAiEndpoint, requestContent);
chatResponse.EnsureSuccessStatusCode();
using var responseJson = await JsonDocument.ParseAsync(await chatResponse.Content.ReadAsStreamAsync());
var result = responseJson.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
return result ?? "No response from model";
}
#endregion
}
}

View File

@ -11,9 +11,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
public class OpenAiService
{
private readonly OpenAiSettings _settings;
private readonly FruitBankSettings _settings;
public OpenAiService(OpenAiSettings settings)
public OpenAiService(FruitBankSettings settings)
{
_settings = settings;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More