442 lines
18 KiB
C#
442 lines
18 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Nop.Core;
|
|
using Nop.Core.Domain.Orders;
|
|
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
|
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
|
using Nop.Services.Catalog;
|
|
using Nop.Services.Customers;
|
|
using Nop.Services.Localization;
|
|
using Nop.Services.Orders;
|
|
using Nop.Web.Framework.Controllers;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
|
|
{
|
|
[AutoValidateAntiforgeryToken]
|
|
public class QuickOrderController : BasePluginController
|
|
{
|
|
private readonly IWorkContext _workContext;
|
|
private readonly IStoreContext _storeContext;
|
|
private readonly IProductService _productService;
|
|
private readonly IShoppingCartService _shoppingCartService;
|
|
private readonly ICustomerService _customerService;
|
|
private readonly ILocalizationService _localizationService;
|
|
private readonly CustomPriceCalculationService _customPriceCalculationService;
|
|
private readonly OpenAIApiService _aiApiService;
|
|
private readonly CerebrasAPIService _cerebrasApiService;
|
|
private readonly FruitBankDbContext _dbContext;
|
|
|
|
// Resource key prefix
|
|
private const string Prefix = "Plugins.Misc.FruitBankPlugin.QuickOrder.";
|
|
|
|
public QuickOrderController(
|
|
IWorkContext workContext,
|
|
IStoreContext storeContext,
|
|
IProductService productService,
|
|
IShoppingCartService shoppingCartService,
|
|
ICustomerService customerService,
|
|
ILocalizationService localizationService,
|
|
IPriceCalculationService priceCalculationService,
|
|
OpenAIApiService aiApiService,
|
|
CerebrasAPIService cerebrasApiService,
|
|
FruitBankDbContext dbContext)
|
|
{
|
|
_workContext = workContext;
|
|
_storeContext = storeContext;
|
|
_productService = productService;
|
|
_shoppingCartService = shoppingCartService;
|
|
_customerService = customerService;
|
|
_localizationService = localizationService;
|
|
_customPriceCalculationService = priceCalculationService as CustomPriceCalculationService;
|
|
_aiApiService = aiApiService;
|
|
_cerebrasApiService = cerebrasApiService;
|
|
_dbContext = dbContext;
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> Index()
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Challenge();
|
|
|
|
return View("~/Plugins/Misc.FruitBankPlugin/Views/QuickOrder/Index.cshtml");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return all available products with prices (for initial page load)
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetAllProducts()
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = await L("NotLoggedIn") });
|
|
|
|
try
|
|
{
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var allProductDtos = (await _dbContext.ProductDtos.GetAll(true).ToListAsync()).Where(pd => pd.AvailableQuantity > 0);
|
|
|
|
//var dbProducts = await _productService.SearchProductsAsync(
|
|
// pageIndex: 0,
|
|
// pageSize: 500,
|
|
// orderBy: );
|
|
|
|
var result = new List<object>();
|
|
|
|
foreach (var product in allProductDtos)
|
|
{
|
|
var productDto = allProductDtos.FirstOrDefault(x => x.Id == product.Id);
|
|
if (productDto == null) continue;
|
|
|
|
var availableQty = product.StockQuantity + productDto.IncomingQuantity;
|
|
if (availableQty <= 0) continue;
|
|
|
|
decimal? unitPrice = null;
|
|
if (!productDto.IsMeasurable)
|
|
{
|
|
var tproduct = await _productService.GetProductByIdAsync(productDto.Id);
|
|
var priceResult = await _customPriceCalculationService.GetFinalPriceAsync(
|
|
tproduct, customer, store, null, 0, true, 1, null, null);
|
|
unitPrice = priceResult.finalPrice;
|
|
}
|
|
|
|
result.Add(new
|
|
{
|
|
id = product.Id,
|
|
name = product.Name,
|
|
quantity = 1,
|
|
requestedQuantity = 1,
|
|
unitPrice,
|
|
stockQuantity = availableQty,
|
|
searchTerm = (string)null,
|
|
isQuantityReduced = false,
|
|
isMeasurable = productDto.IsMeasurable
|
|
});
|
|
}
|
|
|
|
return Json(new { success = true, products = result });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[QuickOrder] GetAllProducts error: {ex.Message}");
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a manually typed product list and return matching products with prices
|
|
/// </summary>
|
|
[HttpPost]
|
|
public async Task<IActionResult> SearchProducts(string text)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = await L("NotLoggedIn") });
|
|
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
return Json(new { success = false, message = await L("NoTextProvided") });
|
|
|
|
try
|
|
{
|
|
var parsedProducts = await ParseProductsFromText(text);
|
|
if (parsedProducts == null || parsedProducts.Count == 0)
|
|
return Json(new { success = false, message = await L("NoProductsIdentified"), transcription = text });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var enrichedProducts = await EnrichProductData(parsedProducts, customer, store);
|
|
|
|
return Json(new { success = true, transcription = text, products = enrichedProducts });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[QuickOrder] SearchProducts error: {ex.Message}");
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transcribe voice audio (Hungarian) then parse and match products
|
|
/// </summary>
|
|
[HttpPost]
|
|
public async Task<IActionResult> TranscribeAndSearch(IFormFile audioFile)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = await L("NotLoggedIn") });
|
|
|
|
if (audioFile == null || audioFile.Length == 0)
|
|
return Json(new { success = false, message = await L("NoAudioReceived") });
|
|
|
|
try
|
|
{
|
|
var transcribedText = await TranscribeAudioFile(audioFile, "hu");
|
|
if (string.IsNullOrEmpty(transcribedText))
|
|
return Json(new { success = false, message = await L("TranscriptionFailed") });
|
|
|
|
Console.WriteLine($"[QuickOrder] Transcription: {transcribedText}");
|
|
|
|
var parsedProducts = await ParseProductsFromText(transcribedText);
|
|
if (parsedProducts == null || parsedProducts.Count == 0)
|
|
return Json(new { success = false, message = await L("NoProductsIdentified"), transcription = transcribedText });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var enrichedProducts = await EnrichProductData(parsedProducts, customer, store);
|
|
|
|
return Json(new { success = true, transcription = transcribedText, products = enrichedProducts });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[QuickOrder] TranscribeAndSearch error: {ex.Message}");
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a product to the current customer's shopping cart and return the updated cart
|
|
/// </summary>
|
|
[HttpPost]
|
|
public async Task<IActionResult> AddToCart(int productId, int quantity)
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false, message = await L("NotLoggedIn") });
|
|
|
|
if (productId <= 0 || quantity <= 0)
|
|
return Json(new { success = false, message = await L("InvalidProductOrQuantity") });
|
|
|
|
try
|
|
{
|
|
var product = await _productService.GetProductByIdAsync(productId);
|
|
if (product == null || product.Deleted || !product.Published)
|
|
return Json(new { success = false, message = await L("ProductNotAvailable") });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
|
|
var warnings = await _shoppingCartService.AddToCartAsync(
|
|
customer: customer,
|
|
product: product,
|
|
shoppingCartType: ShoppingCartType.ShoppingCart,
|
|
storeId: store.Id,
|
|
quantity: quantity);
|
|
|
|
if (warnings.Any())
|
|
return Json(new { success = false, message = string.Join("; ", warnings) });
|
|
|
|
var cartItems = await GetCartItemsJson(customer, store);
|
|
return Json(new { success = true, cartItems });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[QuickOrder] AddToCart error: {ex.Message}");
|
|
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the current customer's cart as JSON (for cart panel refresh)
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetCartItems()
|
|
{
|
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
|
if (await _customerService.IsGuestAsync(customer))
|
|
return Json(new { success = false });
|
|
|
|
var store = await _storeContext.GetCurrentStoreAsync();
|
|
var cartItems = await GetCartItemsJson(customer, store);
|
|
return Json(new { success = true, cartItems });
|
|
}
|
|
|
|
#region Private helpers
|
|
|
|
/// <summary>Shorthand: get a localized QuickOrder resource string</summary>
|
|
private Task<string> L(string keySuffix)
|
|
=> _localizationService.GetResourceAsync(Prefix + keySuffix);
|
|
|
|
private async Task<string> TranscribeAudioFile(IFormFile audioFile, string language)
|
|
{
|
|
var fileName = $"quick_order_{DateTime.Now:yyyyMMdd_HHmmss}.webm";
|
|
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "voice");
|
|
|
|
if (!Directory.Exists(uploadsFolder))
|
|
Directory.CreateDirectory(uploadsFolder);
|
|
|
|
var filePath = Path.Combine(uploadsFolder, fileName);
|
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
|
await audioFile.CopyToAsync(stream);
|
|
|
|
string transcribedText;
|
|
using (var audioStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
|
transcribedText = await _aiApiService.TranscribeAudioAsync(audioStream, fileName, language, null);
|
|
|
|
if (!string.IsNullOrEmpty(transcribedText) &&
|
|
(transcribedText.EndsWith(".") || transcribedText.EndsWith("!") || transcribedText.EndsWith("?")))
|
|
transcribedText = transcribedText[..^1];
|
|
|
|
try { System.IO.File.Delete(filePath); } catch { /* ignore cleanup errors */ }
|
|
return transcribedText;
|
|
}
|
|
|
|
private async Task<List<ParsedProduct>> ParseProductsFromText(string text)
|
|
{
|
|
var systemPrompt = @"You are a product parser for a Hungarian fruit and vegetable wholesale company.
|
|
Parse the product names and quantities from the user's input.
|
|
|
|
CRITICAL RULES:
|
|
1. Extract product names and quantities from ANY produce item
|
|
2. Normalize product names to singular, lowercase (e.g., 'narancsok' → 'narancs')
|
|
3. Handle Hungarian number words ('száz' = 100, 'ötven' = 50, 'húsz' = 20, 'tíz' = 10, 'öt' = 5, 'egy' = 1)
|
|
4. Fix common transcription/typing errors (e.g., 'datója' → 'datolya', 'szűlő' → 'szőlő', 'mondarin' → 'mandarin')
|
|
5. Return ONLY valid JSON array, no explanations
|
|
6. DO NOT include units - only product name and quantity as a number
|
|
7. ALWAYS return at least one product if you can parse anything from the input
|
|
|
|
OUTPUT FORMAT (JSON only):
|
|
[
|
|
{""product"": ""narancs"", ""quantity"": 100},
|
|
{""product"": ""alma"", ""quantity"": 50}
|
|
]";
|
|
|
|
var aiResponse = await _cerebrasApiService.GetSimpleResponseAsync(systemPrompt, $"Parse this: {text}");
|
|
Console.WriteLine($"[QuickOrder] AI parse response: {aiResponse}");
|
|
|
|
var jsonMatch = Regex.Match(aiResponse, @"\[.*\]", RegexOptions.Singleline);
|
|
if (!jsonMatch.Success) return new List<ParsedProduct>();
|
|
|
|
try
|
|
{
|
|
return System.Text.Json.JsonSerializer.Deserialize<List<ParsedProduct>>(jsonMatch.Value)
|
|
?? new List<ParsedProduct>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[QuickOrder] JSON parse error: {ex.Message}");
|
|
return new List<ParsedProduct>();
|
|
}
|
|
}
|
|
|
|
private async Task<List<object>> EnrichProductData(
|
|
List<ParsedProduct> parsedProducts,
|
|
Nop.Core.Domain.Customers.Customer customer,
|
|
Nop.Core.Domain.Stores.Store store)
|
|
{
|
|
var enrichedProducts = new List<object>();
|
|
var allProductDtos = await _dbContext.ProductDtos.GetAll(true).ToListAsync();
|
|
|
|
foreach (var parsed in parsedProducts)
|
|
{
|
|
var dbProducts = await _productService.SearchProductsAsync(
|
|
keywords: parsed.Product,
|
|
pageIndex: 0,
|
|
pageSize: 20);
|
|
|
|
if (!dbProducts.Any())
|
|
{
|
|
Console.WriteLine($"[QuickOrder] No products found for: {parsed.Product}");
|
|
continue;
|
|
}
|
|
|
|
foreach (var product in dbProducts)
|
|
{
|
|
var productDto = allProductDtos.FirstOrDefault(x => x.Id == product.Id);
|
|
if (productDto == null) continue;
|
|
|
|
var availableQty = product.StockQuantity + productDto.IncomingQuantity;
|
|
if (availableQty <= 0) continue;
|
|
|
|
var requestedQty = parsed.Quantity;
|
|
var finalQty = Math.Min(requestedQty, availableQty);
|
|
var isReduced = finalQty < requestedQty;
|
|
|
|
decimal? unitPrice = null;
|
|
if (!productDto.IsMeasurable)
|
|
{
|
|
var priceResult = await _customPriceCalculationService.GetFinalPriceAsync(
|
|
product, customer, store, null, 0, true, finalQty, null, null);
|
|
unitPrice = priceResult.finalPrice;
|
|
}
|
|
|
|
enrichedProducts.Add(new
|
|
{
|
|
id = product.Id,
|
|
name = product.Name,
|
|
quantity = finalQty,
|
|
requestedQuantity = requestedQty,
|
|
unitPrice,
|
|
stockQuantity = availableQty,
|
|
searchTerm = parsed.Product,
|
|
isQuantityReduced = isReduced,
|
|
isMeasurable = productDto.IsMeasurable
|
|
});
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"[QuickOrder] Enriched product count: {enrichedProducts.Count}");
|
|
return enrichedProducts;
|
|
}
|
|
|
|
private async Task<List<object>> GetCartItemsJson(
|
|
Nop.Core.Domain.Customers.Customer customer,
|
|
Nop.Core.Domain.Stores.Store store)
|
|
{
|
|
var cart = await _shoppingCartService.GetShoppingCartAsync(
|
|
customer, ShoppingCartType.ShoppingCart, store.Id);
|
|
|
|
var allProductDtos = await _dbContext.ProductDtos.GetAll(true).ToListAsync();
|
|
var result = new List<object>();
|
|
|
|
foreach (var item in cart)
|
|
{
|
|
var product = await _productService.GetProductByIdAsync(item.ProductId);
|
|
if (product == null) continue;
|
|
|
|
var productDto = allProductDtos.FirstOrDefault(x => x.Id == product.Id);
|
|
var isMeasurable = productDto?.IsMeasurable ?? false;
|
|
|
|
decimal? unitPrice = null;
|
|
if (!isMeasurable)
|
|
{
|
|
var priceResult = await _customPriceCalculationService.GetFinalPriceAsync(
|
|
product, customer, store, null, 0, true, item.Quantity, null, null);
|
|
unitPrice = priceResult.finalPrice;
|
|
}
|
|
|
|
result.Add(new
|
|
{
|
|
id = item.Id,
|
|
productId = item.ProductId,
|
|
name = product.Name,
|
|
quantity = item.Quantity,
|
|
unitPrice,
|
|
isMeasurable
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Inner models
|
|
|
|
private class ParsedProduct
|
|
{
|
|
[System.Text.Json.Serialization.JsonPropertyName("product")]
|
|
public string Product { get; set; }
|
|
|
|
[System.Text.Json.Serialization.JsonPropertyName("quantity")]
|
|
public int Quantity { get; set; }
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|