fixes
This commit is contained in:
parent
9b2e34f7b5
commit
c1e27e12b4
|
|
@ -0,0 +1,75 @@
|
||||||
|
using FruitBank.Common.Enums;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||||
|
using Nop.Services.Security;
|
||||||
|
using Nop.Web.Framework.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers;
|
||||||
|
|
||||||
|
public partial class PreorderAdminController
|
||||||
|
{
|
||||||
|
[HttpPost, Route("Admin/Preorders/UpdateHeader/{id:int}")]
|
||||||
|
public async Task<IActionResult> UpdateHeader(int id, string deliveryDateTime, string? customerNote)
|
||||||
|
{
|
||||||
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Forbid();
|
||||||
|
var p = await _preorderDbContext.Preorders.GetByIdAsync(id);
|
||||||
|
if (p == null || p.Status == PreorderStatus.Cancelled) return Json(new { success = false });
|
||||||
|
if (!DateTime.TryParse(deliveryDateTime, out var d)) return Json(new { success = false, error = "Érvénytelen dátum" });
|
||||||
|
p.DateOfReceipt = d; p.CustomerNote = customerNote?.Trim(); p.UpdatedOnUtc = DateTime.UtcNow;
|
||||||
|
await _preorderDbContext.Preorders.UpdateAsync(p);
|
||||||
|
return Json(new { success = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, Route("Admin/Preorders/UpdateItem/{id:int}")]
|
||||||
|
public async Task<IActionResult> UpdateItem(int id, int quantity, decimal unitPrice)
|
||||||
|
{
|
||||||
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Forbid();
|
||||||
|
var item = await _preorderDbContext.PreorderItems.GetByIdAsync(id);
|
||||||
|
if (item == null || item.FulfilledQuantity > 0) return Json(new { success = false, error = "Módosítás nem lehetséges" });
|
||||||
|
item.RequestedQuantity = Math.Max(1, quantity);
|
||||||
|
item.UnitPriceInclTax = Math.Max(0, unitPrice);
|
||||||
|
await _preorderDbContext.PreorderItems.UpdateAsync(item);
|
||||||
|
return Json(new { success = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, Route("Admin/Preorders/ReplaceItemProduct/{id:int}")]
|
||||||
|
public async Task<IActionResult> ReplaceItemProduct(int id, int newProductId)
|
||||||
|
{
|
||||||
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Forbid();
|
||||||
|
var item = await _preorderDbContext.PreorderItems.GetByIdAsync(id);
|
||||||
|
if (item == null || item.FulfilledQuantity > 0) return Json(new { success = false, error = "Csere nem lehetséges" });
|
||||||
|
var product = await _dbContext.Products.GetByIdAsync(newProductId);
|
||||||
|
if (product == null || product.Deleted) return Json(new { success = false, error = "Termék nem található" });
|
||||||
|
|
||||||
|
item.ProductId = newProductId;
|
||||||
|
|
||||||
|
// Recalculate price for the preorder's customer using CustomPriceCalculationService
|
||||||
|
var preorder = await _preorderDbContext.Preorders.GetByIdAsync(item.PreorderId);
|
||||||
|
var customer = preorder != null ? await _customerService.GetCustomerByIdAsync(preorder.CustomerId) : null;
|
||||||
|
if (customer != null)
|
||||||
|
{
|
||||||
|
var store = await _storeContext.GetCurrentStoreAsync();
|
||||||
|
var calc = await _priceCalculationService.GetFinalPriceAsync(product, customer, store, includeDiscounts: true);
|
||||||
|
item.UnitPriceInclTax = calc.finalPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _preorderDbContext.PreorderItems.UpdateAsync(item);
|
||||||
|
return Json(new { success = true, productName = product.Name, newPrice = item.UnitPriceInclTax });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, Route("Admin/Preorders/ConvertNow/{id:int}")]
|
||||||
|
public async Task<IActionResult> ConvertNow(int id)
|
||||||
|
{
|
||||||
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Forbid();
|
||||||
|
var preorder = await _preorderDbContext.Preorders.GetByIdAsync(id);
|
||||||
|
if (preorder == null || preorder.Status == PreorderStatus.Cancelled)
|
||||||
|
return Json(new { success = false, error = "Konvertálás nem lehetséges" });
|
||||||
|
var pending = await _preorderDbContext.PreorderItems.GetAll()
|
||||||
|
.Where(i => i.PreorderId == id && i.FulfilledQuantity < i.RequestedQuantity).ToListAsync();
|
||||||
|
if (!pending.Any()) return Json(new { success = false, error = "Nincs teljesítetlen tétel" });
|
||||||
|
await _preorderConversionService.ConvertPreordersForProductsAsync(
|
||||||
|
pending.Select(i => i.ProductId).Distinct().ToList(), 0);
|
||||||
|
var refreshed = await _preorderDbContext.Preorders.GetByIdAsync(id);
|
||||||
|
return Json(new { success = true, orderId = refreshed?.OrderId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Nop.Core;
|
||||||
using FruitBank.Common.Entities;
|
using FruitBank.Common.Entities;
|
||||||
using FruitBank.Common.Enums;
|
using FruitBank.Common.Enums;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
@ -5,6 +6,7 @@ using Nop.Core.Domain.Customers;
|
||||||
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
|
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
|
||||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||||
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||||
|
using Nop.Services.Catalog;
|
||||||
using Nop.Services.Customers;
|
using Nop.Services.Customers;
|
||||||
using Nop.Services.Security;
|
using Nop.Services.Security;
|
||||||
using Nop.Web.Framework;
|
using Nop.Web.Framework;
|
||||||
|
|
@ -16,13 +18,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers;
|
||||||
[AuthorizeAdmin]
|
[AuthorizeAdmin]
|
||||||
[Area(AreaNames.ADMIN)]
|
[Area(AreaNames.ADMIN)]
|
||||||
[AutoValidateAntiforgeryToken]
|
[AutoValidateAntiforgeryToken]
|
||||||
public class PreorderAdminController : BasePluginController
|
public partial class PreorderAdminController : BasePluginController
|
||||||
{
|
{
|
||||||
private readonly IPermissionService _permissionService;
|
private readonly IPermissionService _permissionService;
|
||||||
private readonly PreorderDbContext _preorderDbContext;
|
private readonly PreorderDbContext _preorderDbContext;
|
||||||
private readonly FruitBankDbContext _dbContext;
|
private readonly FruitBankDbContext _dbContext;
|
||||||
private readonly ICustomerService _customerService;
|
private readonly ICustomerService _customerService;
|
||||||
private readonly PreorderConversionService _preorderConversionService;
|
private readonly PreorderConversionService _preorderConversionService;
|
||||||
|
private readonly IPriceCalculationService _priceCalculationService;
|
||||||
|
private readonly IStoreContext _storeContext;
|
||||||
|
|
||||||
private static readonly Dictionary<PreorderStatus, string> StatusLabels = new()
|
private static readonly Dictionary<PreorderStatus, string> StatusLabels = new()
|
||||||
{
|
{
|
||||||
|
|
@ -45,13 +49,17 @@ public class PreorderAdminController : BasePluginController
|
||||||
PreorderDbContext preorderDbContext,
|
PreorderDbContext preorderDbContext,
|
||||||
FruitBankDbContext dbContext,
|
FruitBankDbContext dbContext,
|
||||||
ICustomerService customerService,
|
ICustomerService customerService,
|
||||||
PreorderConversionService preorderConversionService)
|
PreorderConversionService preorderConversionService,
|
||||||
|
IPriceCalculationService priceCalculationService,
|
||||||
|
IStoreContext storeContext)
|
||||||
{
|
{
|
||||||
_permissionService = permissionService;
|
_permissionService = permissionService;
|
||||||
_preorderDbContext = preorderDbContext;
|
_preorderDbContext = preorderDbContext;
|
||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
_customerService = customerService;
|
_customerService = customerService;
|
||||||
_preorderConversionService = preorderConversionService;
|
_preorderConversionService = preorderConversionService;
|
||||||
|
_priceCalculationService = priceCalculationService;
|
||||||
|
_storeContext = storeContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LIST PAGE ─────────────────────────────────────────────────────────────
|
// ── LIST PAGE ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -121,11 +129,12 @@ public class PreorderAdminController : BasePluginController
|
||||||
|
|
||||||
// Derive a display status: use the DB enum if it looks valid (non-zero),
|
// Derive a display status: use the DB enum if it looks valid (non-zero),
|
||||||
// otherwise infer from quantities
|
// otherwise infer from quantities
|
||||||
var effectiveStatus = (int)p.Status != 0
|
var effectiveStatus =
|
||||||
? p.Status
|
p.Status == PreorderStatus.Cancelled ? PreorderStatus.Cancelled :
|
||||||
: allFulfilled ? PreorderStatus.Confirmed
|
(int)p.Status != 0 ? p.Status :
|
||||||
: anyFulfilled ? PreorderStatus.PartiallyFulfilled
|
allFulfilled ? PreorderStatus.Confirmed :
|
||||||
: PreorderStatus.Pending;
|
anyFulfilled || p.OrderId.HasValue ? PreorderStatus.PartiallyFulfilled :
|
||||||
|
PreorderStatus.Pending;
|
||||||
|
|
||||||
return new PreorderListRow
|
return new PreorderListRow
|
||||||
{
|
{
|
||||||
|
|
@ -357,10 +366,13 @@ public class PreorderAdminController : BasePluginController
|
||||||
if (preorder == null)
|
if (preorder == null)
|
||||||
return Json(new { success = false, error = "Preorder not found" });
|
return Json(new { success = false, error = "Preorder not found" });
|
||||||
|
|
||||||
if (preorder.Status != PreorderStatus.Pending)
|
if (preorder.Status == PreorderStatus.Cancelled)
|
||||||
return Json(new { success = false, error = "Only pending preorders can be cancelled" });
|
return Json(new { success = false, error = "Már visszavont előrendelés" });
|
||||||
|
|
||||||
|
preorder.Status = PreorderStatus.Cancelled;
|
||||||
|
preorder.UpdatedOnUtc = DateTime.UtcNow;
|
||||||
|
await _preorderDbContext.Preorders.UpdateAsync(preorder);
|
||||||
|
|
||||||
await _preorderDbContext.CancelPreorderAsync(id);
|
|
||||||
return Json(new { success = true });
|
return Json(new { success = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,32 @@ public class OrderController : BasePluginController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── TRANSCRIBE ONLY (for preorder history page filter) ─────────────────
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> TranscribeOnly(Microsoft.AspNetCore.Http.IFormFile audioFile)
|
||||||
|
{
|
||||||
|
var customer = await _workContext.GetCurrentCustomerAsync();
|
||||||
|
if (await _customerService.IsGuestAsync(customer))
|
||||||
|
return Json(new { success = false });
|
||||||
|
|
||||||
|
if (audioFile == null || audioFile.Length == 0)
|
||||||
|
return Json(new { success = false, message = "Nem érkezett hangfájl" });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var text = await TranscribeAudioFile(audioFile, "hu");
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return Json(new { success = false, message = "Nem sikerült a hangfelismerés" });
|
||||||
|
|
||||||
|
return Json(new { success = true, transcription = text });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── ADD TO CART (Quick Order flow) ────────────────────────────────────────
|
// ── ADD TO CART (Quick Order flow) ────────────────────────────────────────
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,7 @@ public partial class PreorderConversionService
|
||||||
BillingAddressId = billingAddressId,
|
BillingAddressId = billingAddressId,
|
||||||
OrderStatusId = (int)OrderStatus.Pending,
|
OrderStatusId = (int)OrderStatus.Pending,
|
||||||
PaymentStatusId = (int)PaymentStatus.Pending,
|
PaymentStatusId = (int)PaymentStatus.Pending,
|
||||||
ShippingStatusId = (int)ShippingStatus.NotYetShipped,
|
ShippingStatusId = (int)ShippingStatus.ShippingNotRequired,
|
||||||
PaymentMethodSystemName = "Payments.CheckMoneyOrder",
|
PaymentMethodSystemName = "Payments.CheckMoneyOrder",
|
||||||
CustomerLanguageId = 1,
|
CustomerLanguageId = 1,
|
||||||
CustomerTaxDisplayTypeId = 0,
|
CustomerTaxDisplayTypeId = 0,
|
||||||
|
|
@ -433,6 +433,9 @@ public partial class PreorderConversionService
|
||||||
|
|
||||||
private async Task InsertOrderItemsAsync(Order order, List<PreorderItem> items)
|
private async Task InsertOrderItemsAsync(Order order, List<PreorderItem> items)
|
||||||
{
|
{
|
||||||
|
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
|
||||||
|
var store = await _storeContext.GetCurrentStoreAsync();
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
var productDto = await _dbContext.ProductDtos.GetByIdAsync(item.ProductId, true);
|
var productDto = await _dbContext.ProductDtos.GetByIdAsync(item.ProductId, true);
|
||||||
|
|
@ -441,8 +444,20 @@ public partial class PreorderConversionService
|
||||||
var product = await _productService.GetProductByIdAsync(item.ProductId);
|
var product = await _productService.GetProductByIdAsync(item.ProductId);
|
||||||
if (product == null) continue;
|
if (product == null) continue;
|
||||||
|
|
||||||
var unitPriceExclTax = Math.Round(item.UnitPriceInclTax / 1.27m, 4);
|
// Recalculate price at conversion time — may have changed since preorder was placed
|
||||||
var priceInclTax = productDto.IsMeasurable ? 0m : item.UnitPriceInclTax * item.FulfilledQuantity;
|
decimal unitPriceInclTax;
|
||||||
|
if (customer != null && _customPriceCalculationService != null)
|
||||||
|
{
|
||||||
|
var calc = await _customPriceCalculationService.GetFinalPriceAsync(
|
||||||
|
product, customer, store, includeDiscounts: true);
|
||||||
|
unitPriceInclTax = calc.finalPrice;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unitPriceInclTax = item.UnitPriceInclTax;
|
||||||
|
}
|
||||||
|
var unitPriceExclTax = Math.Round(unitPriceInclTax / 1.27m, 4);
|
||||||
|
var priceInclTax = productDto.IsMeasurable ? 0m : unitPriceInclTax * item.FulfilledQuantity;
|
||||||
var priceExclTax = productDto.IsMeasurable ? 0m : unitPriceExclTax * item.FulfilledQuantity;
|
var priceExclTax = productDto.IsMeasurable ? 0m : unitPriceExclTax * item.FulfilledQuantity;
|
||||||
|
|
||||||
var orderItem = new OrderItem
|
var orderItem = new OrderItem
|
||||||
|
|
@ -451,7 +466,7 @@ public partial class PreorderConversionService
|
||||||
OrderId = order.Id,
|
OrderId = order.Id,
|
||||||
ProductId = item.ProductId,
|
ProductId = item.ProductId,
|
||||||
Quantity = item.FulfilledQuantity,
|
Quantity = item.FulfilledQuantity,
|
||||||
UnitPriceInclTax = item.UnitPriceInclTax,
|
UnitPriceInclTax = unitPriceInclTax,
|
||||||
UnitPriceExclTax = unitPriceExclTax,
|
UnitPriceExclTax = unitPriceExclTax,
|
||||||
PriceInclTax = priceInclTax,
|
PriceInclTax = priceInclTax,
|
||||||
PriceExclTax = priceExclTax,
|
PriceExclTax = priceExclTax,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,39 @@
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var preorder in Model)
|
<!-- ── Search bar ────────────────────────────────────────────── -->
|
||||||
|
<div class="po-search-wrap">
|
||||||
|
<div class="search-input-group">
|
||||||
|
<button type="button" id="poRecordBtn" class="mic-btn" title="Hangfelvétel indítása">
|
||||||
|
<i class="fa fa-microphone"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="poStopBtn" class="mic-btn mic-btn-recording" style="display:none;" title="Leállítás">
|
||||||
|
<i class="fa fa-stop"></i>
|
||||||
|
</button>
|
||||||
|
<input type="text" id="poSearchInput" class="qo-input"
|
||||||
|
placeholder="Keress termékre az előrendeléseid között..."
|
||||||
|
onkeypress="if(event.key==='Enter') filterPreorders()" />
|
||||||
|
<button type="button" class="qo-search-btn" onclick="filterPreorders()">
|
||||||
|
<i class="fa fa-search"></i> Keresés
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="poRecordingStatus" class="recording-status-bar" style="display:none;">
|
||||||
|
<span id="poStatusText">Figyelés...</span>
|
||||||
|
<div class="volume-bar-container">
|
||||||
|
<div class="volume-bar volume-bar-silent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="poSearchNoResults" class="po-search-empty" style="display:none;">
|
||||||
|
<i class="fa fa-search"></i> Nincs találat a keresési feltételre.
|
||||||
|
<button type="button" class="po-clear-btn" onclick="clearFilter()">
|
||||||
|
<i class="fa fa-times"></i> Mutasd az összeset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Preorder cards ────────────────────────────────────────── -->
|
||||||
|
<div id="poCardList">
|
||||||
|
@foreach (var preorder in Model)
|
||||||
{
|
{
|
||||||
var statusClass = preorder.Status switch
|
var statusClass = preorder.Status switch
|
||||||
{
|
{
|
||||||
|
|
@ -40,8 +72,13 @@
|
||||||
PreorderStatus.Cancelled => "Törölve / Lejárt",
|
PreorderStatus.Cancelled => "Törölve / Lejárt",
|
||||||
_ => "Függőben"
|
_ => "Függőben"
|
||||||
};
|
};
|
||||||
|
// Build searchable text for client-side filtering
|
||||||
|
var searchText = string.Join(" ",
|
||||||
|
preorder.Items.Select(i => i.ProductName)
|
||||||
|
.Append(preorder.CustomerNote ?? "")
|
||||||
|
).ToLowerInvariant();
|
||||||
|
|
||||||
<div class="po-customer-card">
|
<div class="po-customer-card" data-search="@searchText">
|
||||||
<div class="po-card-header">
|
<div class="po-card-header">
|
||||||
<div class="po-card-meta">
|
<div class="po-card-meta">
|
||||||
<span class="po-card-id">#@preorder.PreorderId előrendelés</span>
|
<span class="po-card-id">#@preorder.PreorderId előrendelés</span>
|
||||||
|
|
@ -126,13 +163,47 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* ── Page ─────────────────────────────────────────────────────── */
|
/* ── Search bar ───────────────────────────────────────────────────────── */
|
||||||
|
.po-search-wrap {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.po-search-empty {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff8ee;
|
||||||
|
border: 1px solid #f4c87a;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #7a4200;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.po-clear-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #c87500;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c87500;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.po-clear-btn:hover { background: #fff3cd; }
|
||||||
|
|
||||||
|
/* ── Page ─────────────────────────────────────────────────────────────── */
|
||||||
.my-preorders-page .page-title h1 {
|
.my-preorders-page .page-title h1 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #1a3c22;
|
color: #1a3c22;
|
||||||
|
|
@ -147,13 +218,18 @@
|
||||||
|
|
||||||
.no-data p { margin-bottom: 16px; font-size: 15px; }
|
.no-data p { margin-bottom: 16px; font-size: 15px; }
|
||||||
|
|
||||||
/* ── Preorder card ────────────────────────────────────────────── */
|
/* ── Preorder card ────────────────────────────────────────────────────── */
|
||||||
.po-customer-card {
|
.po-customer-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #dde8da;
|
border: 1px solid #dde8da;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.po-customer-card.po-hidden {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.po-card-header {
|
.po-card-header {
|
||||||
|
|
@ -191,7 +267,6 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status badges */
|
|
||||||
.po-status-badge {
|
.po-status-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 3px 12px;
|
padding: 3px 12px;
|
||||||
|
|
@ -213,10 +288,8 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.po-order-link:hover { background: #2d7a3a; color: #fff; }
|
.po-order-link:hover { background: #2d7a3a; color: #fff; }
|
||||||
|
|
||||||
/* Note */
|
|
||||||
.po-card-note {
|
.po-card-note {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
@ -224,10 +297,8 @@
|
||||||
background: #fffdf7;
|
background: #fffdf7;
|
||||||
border-bottom: 1px solid #dde8da;
|
border-bottom: 1px solid #dde8da;
|
||||||
}
|
}
|
||||||
|
|
||||||
.po-card-note .fa { margin-right: 6px; color: #f4a236; }
|
.po-card-note .fa { margin-right: 6px; color: #f4a236; }
|
||||||
|
|
||||||
/* Items table */
|
|
||||||
.po-card-items { padding: 0; }
|
.po-card-items { padding: 0; }
|
||||||
|
|
||||||
.po-items-table {
|
.po-items-table {
|
||||||
|
|
@ -284,3 +355,149 @@
|
||||||
.po-items-table td:last-child { display: none; }
|
.po-items-table td:last-child { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script asp-location="Footer">
|
||||||
|
// ── Filter ────────────────────────────────────────────────────────────────
|
||||||
|
function filterPreorders() {
|
||||||
|
var term = ($('#poSearchInput').val() || '').trim().toLowerCase();
|
||||||
|
if (!term) { clearFilter(); return; }
|
||||||
|
|
||||||
|
var visible = 0;
|
||||||
|
$('.po-customer-card').each(function () {
|
||||||
|
var searchData = ($(this).attr('data-search') || '').toLowerCase();
|
||||||
|
if (searchData.indexOf(term) !== -1) {
|
||||||
|
$(this).removeClass('po-hidden'); visible++;
|
||||||
|
} else {
|
||||||
|
$(this).addClass('po-hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (visible === 0) {
|
||||||
|
$('#poSearchNoResults').show();
|
||||||
|
} else {
|
||||||
|
$('#poSearchNoResults').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilter() {
|
||||||
|
$('#poSearchInput').val('');
|
||||||
|
$('.po-customer-card').removeClass('po-hidden');
|
||||||
|
$('#poSearchNoResults').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Voice input ───────────────────────────────────────────────────────────
|
||||||
|
var poMediaRecorder = null, poAudioChunks = [], poIsRecording = false;
|
||||||
|
var poAudioContext = null, poAnalyser = null, poVolumeInterval = null;
|
||||||
|
var poRecordingStart = null, poBaselineNoise = -60, poVolumeHistory = [];
|
||||||
|
var VAD = { silenceDuration:1500, minRecordingTime:800, volumeCheckInterval:100,
|
||||||
|
calibrationTime:500, noiseGateOffset:15, volumeHistorySize:10 };
|
||||||
|
|
||||||
|
function getSupportedMimeType() {
|
||||||
|
var types = ['audio/webm','audio/webm;codecs=opus','audio/ogg;codecs=opus','audio/mp4'];
|
||||||
|
for (var i = 0; i < types.length; i++) if (MediaRecorder.isTypeSupported(types[i])) return types[i];
|
||||||
|
return 'audio/webm';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#poRecordBtn').click(function () {
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
alert('A böngésző nem támogatja a hangfelvételt.'); return;
|
||||||
|
}
|
||||||
|
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
|
||||||
|
poAudioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
poAnalyser = poAudioContext.createAnalyser();
|
||||||
|
poAudioContext.createMediaStreamSource(stream).connect(poAnalyser);
|
||||||
|
poAnalyser.fftSize = 512;
|
||||||
|
var mimeType = getSupportedMimeType();
|
||||||
|
poMediaRecorder = new MediaRecorder(stream, { mimeType: mimeType });
|
||||||
|
poAudioChunks = []; poRecordingStart = Date.now(); poIsRecording = true;
|
||||||
|
|
||||||
|
poMediaRecorder.addEventListener('dataavailable', function (e) { poAudioChunks.push(e.data); });
|
||||||
|
poMediaRecorder.addEventListener('stop', function () {
|
||||||
|
var blob = new Blob(poAudioChunks, { type: mimeType });
|
||||||
|
stream.getTracks().forEach(function (t) { t.stop(); });
|
||||||
|
if (poAudioContext) { poAudioContext.close(); poAudioContext = null; }
|
||||||
|
poAnalyser = null; poIsRecording = false;
|
||||||
|
if (blob.size === 0) { resetPoRecordingUI(); return; }
|
||||||
|
transcribeForFilter(blob, mimeType);
|
||||||
|
});
|
||||||
|
poMediaRecorder.start();
|
||||||
|
$('#poRecordBtn').hide(); $('#poStopBtn').show();
|
||||||
|
$('#poSearchInput').attr('placeholder', 'Figyelés... (mondd a keresett termék nevét)');
|
||||||
|
poShowStatus('Figyelés...'); startPoVAD();
|
||||||
|
}).catch(function (err) { alert('Nem sikerült a mikrofon elérése: ' + err.message); });
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#poStopBtn').click(function () { stopPoRecording(); });
|
||||||
|
|
||||||
|
function startPoVAD() {
|
||||||
|
var bufferLength = poAnalyser.frequencyBinCount, dataArray = new Uint8Array(bufferLength);
|
||||||
|
if (poVolumeInterval) clearInterval(poVolumeInterval);
|
||||||
|
var silentChecks = 0, silentNeeded = Math.ceil(VAD.silenceDuration / VAD.volumeCheckInterval);
|
||||||
|
var calibrated = false, calibSamples = []; poVolumeHistory = [];
|
||||||
|
poVolumeInterval = setInterval(function () {
|
||||||
|
if (!poIsRecording || !poAnalyser) { clearInterval(poVolumeInterval); return; }
|
||||||
|
poAnalyser.getByteFrequencyData(dataArray);
|
||||||
|
var sum = 0; for (var i = 0; i < bufferLength; i++) sum += dataArray[i];
|
||||||
|
var volume = 20 * Math.log10((sum / bufferLength) / 255);
|
||||||
|
var elapsed = Date.now() - poRecordingStart;
|
||||||
|
if (!calibrated && elapsed < VAD.calibrationTime) { calibSamples.push(volume); return; }
|
||||||
|
if (!calibrated && calibSamples.length > 0) {
|
||||||
|
var tot = 0; for (var j = 0; j < calibSamples.length; j++) tot += calibSamples[j];
|
||||||
|
poBaselineNoise = tot / calibSamples.length; calibrated = true;
|
||||||
|
}
|
||||||
|
poVolumeHistory.push(volume);
|
||||||
|
if (poVolumeHistory.length > VAD.volumeHistorySize) poVolumeHistory.shift();
|
||||||
|
var vs = 0; for (var k = 0; k < poVolumeHistory.length; k++) vs += poVolumeHistory[k];
|
||||||
|
var avgVol = vs / poVolumeHistory.length;
|
||||||
|
var threshold = poBaselineNoise + VAD.noiseGateOffset;
|
||||||
|
var norm = Math.max(0, Math.min(100, ((volume - threshold + 10) / 40) * 100));
|
||||||
|
var text = 'Figyelés...', cls = 'volume-bar-silent';
|
||||||
|
if (norm > 60) { text = 'Hangos és érthető'; cls = 'volume-bar-high'; }
|
||||||
|
else if (norm > 30) { text = 'Beszél...'; cls = 'volume-bar-medium'; }
|
||||||
|
else if (norm > 10) { text = 'Hangosabban!'; cls = 'volume-bar-low'; }
|
||||||
|
$('#poStatusText').text(text);
|
||||||
|
$('#poRecordingStatus .volume-bar').removeClass('volume-bar-low volume-bar-medium volume-bar-high volume-bar-silent').addClass(cls).css('width', norm + '%');
|
||||||
|
if (elapsed < VAD.minRecordingTime) return;
|
||||||
|
if (avgVol < threshold) { silentChecks++; if (silentChecks >= silentNeeded) { clearInterval(poVolumeInterval); stopPoRecording(); } }
|
||||||
|
else { silentChecks = 0; }
|
||||||
|
}, VAD.volumeCheckInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPoRecording() {
|
||||||
|
if (poVolumeInterval) { clearInterval(poVolumeInterval); poVolumeInterval = null; }
|
||||||
|
if (poMediaRecorder && poMediaRecorder.state !== 'inactive') {
|
||||||
|
poShowStatus('Feldolgozás...'); poMediaRecorder.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transcribeForFilter(blob, mimeType) {
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('audioFile', blob, 'recording.webm');
|
||||||
|
formData.append('__RequestVerificationToken', $('input[name="__RequestVerificationToken"]').val());
|
||||||
|
$.ajax({
|
||||||
|
url : '@Url.Action("TranscribeOnly", "Order")',
|
||||||
|
type : 'POST',
|
||||||
|
data : formData,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
success : function (r) {
|
||||||
|
resetPoRecordingUI();
|
||||||
|
if (r.success && r.transcription) {
|
||||||
|
$('#poSearchInput').val(r.transcription);
|
||||||
|
filterPreorders();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () { resetPoRecordingUI(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPoRecordingUI() {
|
||||||
|
$('#poRecordingStatus').hide();
|
||||||
|
$('#poRecordBtn').show(); $('#poStopBtn').hide();
|
||||||
|
$('#poSearchInput').attr('placeholder', 'Keress termékre az előrendeléseid között...');
|
||||||
|
}
|
||||||
|
|
||||||
|
function poShowStatus(msg) {
|
||||||
|
$('#poStatusText').text(msg); $('#poRecordingStatus').show();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue