Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Views/QuickOrder/Index.cshtml

567 lines
28 KiB
Plaintext

@{
Layout = "_Root";
ViewBag.Title = T("Plugins.Misc.FruitBankPlugin.QuickOrder.PageTitle").Text;
}
<link rel="stylesheet" href="~/Plugins/Misc.FruitBankPlugin/css/quick-order.css" />
<div class="quick-order-page">
<!-- Full-width Search Bar -->
<div class="qo-search-bar-wrapper">
<div class="qo-search-bar">
<div class="search-input-group">
<button id="recordBtn" class="mic-btn" title="@T("Plugins.Misc.FruitBankPlugin.QuickOrder.MicButtonTitle")">
<i class="fa fa-microphone"></i>
</button>
<button id="stopBtn" class="mic-btn mic-btn-recording" style="display:none;" title="@T("Plugins.Misc.FruitBankPlugin.QuickOrder.StopButtonTitle")">
<i class="fa fa-stop"></i>
<span class="mic-pulse"></span>
</button>
<input type="text"
id="searchInput"
class="qo-input"
placeholder="@T("Plugins.Misc.FruitBankPlugin.QuickOrder.SearchPlaceholder")"
onkeypress="if(event.key==='Enter') submitTextSearch()">
<button class="qo-search-btn" onclick="submitTextSearch()">
<i class="fa fa-search"></i> @T("Plugins.Misc.FruitBankPlugin.QuickOrder.SearchButton")
</button>
</div>
<div id="recordingStatus" class="recording-status-bar" style="display:none;">
<span id="statusText">@T("Plugins.Misc.FruitBankPlugin.QuickOrder.ListeningStatus")</span>
<div class="volume-bar-container">
<div class="volume-bar volume-bar-silent"></div>
</div>
</div>
</div>
</div>
<!-- Two-column layout -->
<div class="qo-layout">
<!-- LEFT: Products -->
<div class="qo-products-panel">
<div id="transcribedCard" class="result-card" style="display:none;">
<div class="result-label"><i class="fa fa-microphone"></i> @T("Plugins.Misc.FruitBankPlugin.QuickOrder.TranscribedLabel")</div>
<div id="transcribedText" class="result-text"></div>
</div>
<div id="noResultsCard" class="no-results-card" style="display:none;">
<i class="fa fa-search"></i>
<p>@T("Plugins.Misc.FruitBankPlugin.QuickOrder.NoResultsText")</p>
</div>
<!-- Loading state -->
<div id="productsLoadingState" class="products-empty-state">
<i class="fa fa-spinner fa-spin"></i>
<p>@T("Plugins.Misc.FruitBankPlugin.QuickOrder.LoadingProducts")</p>
</div>
<div id="productMatchesCard" style="display:none;">
<div class="matches-label">
<i class="fa fa-cubes"></i> <span id="matchesLabelText">@T("Plugins.Misc.FruitBankPlugin.QuickOrder.AllProductsLabel")</span>
@T("Plugins.Misc.FruitBankPlugin.QuickOrder.QuantityHint")
</div>
<div id="productButtons" class="product-grid"></div>
</div>
</div>
<!-- RIGHT: Cart -->
<div class="qo-cart-panel">
<div class="qo-section-title">
<i class="fa fa-shopping-basket"></i> @T("Plugins.Misc.FruitBankPlugin.QuickOrder.CartTitle")
<span id="cartItemCount" class="cart-count-badge">0</span>
</div>
<div id="cartEmptyState" class="cart-empty">
<i class="fa fa-shopping-basket"></i>
<p>@T("Plugins.Misc.FruitBankPlugin.QuickOrder.CartEmptyLine1")<br>@T("Plugins.Misc.FruitBankPlugin.QuickOrder.CartEmptyLine2")</p>
</div>
<div id="cartItemsList" class="cart-items-list" style="display:none;"></div>
<div id="cartTotalRow" class="cart-total-row" style="display:none;">
<div class="cart-total-note">
<i class="fa fa-info-circle"></i>
<small>@T("Plugins.Misc.FruitBankPlugin.QuickOrder.CartTotalNote")</small>
</div>
<div class="cart-total">
<span>@T("Plugins.Misc.FruitBankPlugin.QuickOrder.EstimatedTotal")</span>
<strong id="cartTotalAmount">0 Ft</strong>
</div>
</div>
<div id="cartActions" style="display:none;">
<a href="@Url.Action("Cart", "ShoppingCart")" class="btn-checkout">
<i class="fa fa-shopping-cart"></i> @T("Plugins.Misc.FruitBankPlugin.QuickOrder.CheckoutButton")
</a>
<a href="@Url.Action("Cart", "ShoppingCart")" class="btn-view-cart">
<i class="fa fa-eye"></i> @T("Plugins.Misc.FruitBankPlugin.QuickOrder.ViewCartButton")
</a>
</div>
</div>
</div>
</div>
@Html.AntiForgeryToken()
@* JS string bundle — Razor renders these once so JS never contains raw Hungarian *@
<script asp-location="Footer">
var qoStr = {
allProducts: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.AllProductsLabel").Text))',
searchResults: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.SearchResultsLabel").Text))',
searchPlaceholder: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.SearchPlaceholder").Text))',
activeRecordingPlaceholder: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.ActiveRecordingPlaceholder").Text))',
listeningStatus: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.ListeningStatus").Text))',
browserNotSupported: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.BrowserNotSupported").Text))',
micAccessError: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.MicAccessError").Text))',
micPermissionDenied: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.MicPermissionDenied").Text))',
micNotFound: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.MicNotFound").Text))',
calibrating: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.Calibrating").Text))',
processing: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.Processing").Text))',
recordingFailed: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.RecordingFailed").Text))',
volumeHigh: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.VolumeHigh").Text))',
volumeSpeaking: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.VolumeSpeaking").Text))',
volumeLouder: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.VolumeLouder").Text))',
searching: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.Searching").Text))',
enterProducts: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.EnterProducts").Text))',
searchError: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.SearchError").Text))',
audioError: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.AudioError").Text))',
addToCartError: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.AddToCartError").Text))',
errorPrefix: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.ErrorPrefix").Text))',
measurableBadge: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.MeasurableBadge").Text))',
stockLabel: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.StockLabel").Text))',
stockLimitedPrefix: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.StockLimitedPrefix").Text))',
stockLimitedSuffix: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.StockLimitedSuffix").Text))',
pieceUnit: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.PieceUnit").Text))',
pricePerPiece: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.PricePerPiece").Text))',
addToCartTitle: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.AddToCartTitle").Text))',
addedToCart: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.AddedToCart").Text))'
};
</script>
<script asp-location="Footer">
var mediaRecorder = null;
var audioChunks = [];
var isRecording = false;
var audioContext = null;
var analyser = null;
var volumeCheckInterval = null;
var recordingStartTime = null;
var baselineNoiseLevel = -60;
var volumeHistory = [];
var VAD_CONFIG = {
silenceDuration: 1500,
minRecordingTime: 800,
volumeCheckInterval: 100,
calibrationTime: 500,
noiseGateOffset: 15,
volumeHistorySize: 10
};
$(document).ready(function () {
$('#recordBtn').click(startRecording);
$('#stopBtn').click(function () { stopRecording(false); });
loadCart();
loadAllProducts();
});
// ── Product list ──────────────────────────────────────────────────────────
function loadAllProducts() {
$('#transcribedCard').hide();
$('#noResultsCard').hide();
$('#productMatchesCard').hide();
$('#productsLoadingState').show();
$('#matchesLabelText').text(qoStr.allProducts);
$.ajax({
url: '@Url.Action("GetAllProducts", "QuickOrder")',
type: 'GET',
success: function (result) {
$('#productsLoadingState').hide();
if (result.success && result.products && result.products.length > 0) {
displayProductMatches(result.products);
} else {
$('#noResultsCard').show();
}
},
error: function () {
$('#productsLoadingState').hide();
$('#noResultsCard').show();
}
});
}
// ── Voice recording ───────────────────────────────────────────────────────
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';
}
function startRecording() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert(qoStr.browserNotSupported);
return;
}
navigator.mediaDevices.getUserMedia({ audio: true })
.then(function (stream) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
audioContext.createMediaStreamSource(stream).connect(analyser);
analyser.fftSize = 512;
var mimeType = getSupportedMimeType();
mediaRecorder = new MediaRecorder(stream, { mimeType: mimeType });
audioChunks = [];
recordingStartTime = Date.now();
isRecording = true;
mediaRecorder.addEventListener('dataavailable', function (e) { audioChunks.push(e.data); });
mediaRecorder.addEventListener('stop', function () {
var blob = new Blob(audioChunks, { type: mimeType });
stream.getTracks().forEach(function (t) { t.stop(); });
if (audioContext) { audioContext.close(); audioContext = null; }
analyser = null;
isRecording = false;
if (blob.size === 0) {
alert(qoStr.recordingFailed);
resetRecordingUI();
return;
}
processAudio(blob, mimeType);
});
mediaRecorder.start();
$('#recordBtn').hide();
$('#stopBtn').show();
$('#searchInput').attr('placeholder', qoStr.activeRecordingPlaceholder);
showStatus(qoStr.activeRecordingPlaceholder);
startVAD();
})
.catch(function (err) {
var msg = qoStr.micAccessError;
if (err.name === 'NotAllowedError') msg += qoStr.micPermissionDenied;
else if (err.name === 'NotFoundError') msg += qoStr.micNotFound;
else msg += err.message;
alert(msg);
});
}
function startVAD() {
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
if (volumeCheckInterval) clearInterval(volumeCheckInterval);
var silentChecks = 0;
var silentNeeded = Math.ceil(VAD_CONFIG.silenceDuration / VAD_CONFIG.volumeCheckInterval);
var calibrated = false;
var calibSamples = [];
volumeHistory = [];
volumeCheckInterval = setInterval(function () {
if (!isRecording || !analyser) { clearInterval(volumeCheckInterval); return; }
analyser.getByteFrequencyData(dataArray);
var sum = 0;
for (var i = 0; i < bufferLength; i++) sum += dataArray[i];
var avg = sum / bufferLength;
var volume = 20 * Math.log10(avg / 255);
var elapsed = Date.now() - recordingStartTime;
if (!calibrated && elapsed < VAD_CONFIG.calibrationTime) {
calibSamples.push(volume);
updateVolumeBar(volume, false, qoStr.calibrating);
return;
}
if (!calibrated && calibSamples.length > 0) {
var total = 0;
for (var j = 0; j < calibSamples.length; j++) total += calibSamples[j];
baselineNoiseLevel = total / calibSamples.length;
calibrated = true;
}
volumeHistory.push(volume);
if (volumeHistory.length > VAD_CONFIG.volumeHistorySize) volumeHistory.shift();
var volSum = 0;
for (var k = 0; k < volumeHistory.length; k++) volSum += volumeHistory[k];
var avgVol = volSum / volumeHistory.length;
var threshold = baselineNoiseLevel + VAD_CONFIG.noiseGateOffset;
updateVolumeBar(volume, true, null);
if (elapsed < VAD_CONFIG.minRecordingTime) return;
if (avgVol < threshold) {
silentChecks++;
if (silentChecks >= silentNeeded) { clearInterval(volumeCheckInterval); stopRecording(true); }
} else {
silentChecks = 0;
}
}, VAD_CONFIG.volumeCheckInterval);
}
function updateVolumeBar(volume, active, customMsg) {
var threshold = baselineNoiseLevel + VAD_CONFIG.noiseGateOffset;
var norm = Math.max(0, Math.min(100, ((volume - threshold + 10) / 40) * 100));
var text = customMsg || qoStr.listeningStatus;
var cls = 'volume-bar-silent';
if (active && !customMsg) {
if (norm > 60) { text = qoStr.volumeHigh; cls = 'volume-bar-high'; }
else if (norm > 30) { text = qoStr.volumeSpeaking; cls = 'volume-bar-medium'; }
else if (norm > 10) { text = qoStr.volumeLouder; cls = 'volume-bar-low'; }
else { text = qoStr.listeningStatus; }
}
$('#statusText').text(text);
$('#recordingStatus .volume-bar')
.removeClass('volume-bar-low volume-bar-medium volume-bar-high volume-bar-silent')
.addClass(cls).css('width', norm + '%');
}
function stopRecording(auto) {
if (volumeCheckInterval) { clearInterval(volumeCheckInterval); volumeCheckInterval = null; }
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
showStatus(qoStr.processing);
mediaRecorder.stop();
}
}
function processAudio(blob, mimeType) {
var formData = new FormData();
formData.append('audioFile', blob, 'recording.webm');
formData.append('__RequestVerificationToken', $('input[name="__RequestVerificationToken"]').val());
$.ajax({
url: '@Url.Action("TranscribeAndSearch", "QuickOrder")',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (result) { resetRecordingUI(); handleSearchResult(result); },
error: function (err) { resetRecordingUI(); alert(qoStr.audioError); console.error(err); }
});
}
function resetRecordingUI() {
$('#recordingStatus').hide();
$('#recordBtn').show();
$('#stopBtn').hide();
$('#searchInput').attr('placeholder', qoStr.searchPlaceholder);
}
function showStatus(msg) {
$('#statusText').text(msg);
$('#recordingStatus').show();
}
// ── Search ────────────────────────────────────────────────────────────────
function submitTextSearch() {
var text = $('#searchInput').val().trim();
if (!text) { alert(qoStr.enterProducts); return; }
showStatus(qoStr.searching);
$('#recordingStatus').show();
$.ajax({
url: '@Url.Action("SearchProducts", "QuickOrder")',
type: 'POST',
data: { text: text, __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val() },
success: function (result) { $('#recordingStatus').hide(); handleSearchResult(result); },
error: function () { $('#recordingStatus').hide(); alert(qoStr.searchError); }
});
}
function handleSearchResult(result) {
$('#noResultsCard').hide();
$('#productMatchesCard').hide();
$('#transcribedCard').hide();
$('#productsLoadingState').hide();
if (!result.success) { alert(qoStr.errorPrefix + result.message); return; }
if (result.transcription) { $('#transcribedText').text(result.transcription); $('#transcribedCard').show(); }
if (!result.products || result.products.length === 0) { $('#noResultsCard').show(); return; }
$('#matchesLabelText').text(qoStr.searchResults);
displayProductMatches(result.products);
}
// ── Product cards ─────────────────────────────────────────────────────────
function displayProductMatches(products) {
var container = $('#productButtons').empty();
var grouped = {};
for (var i = 0; i < products.length; i++) {
var key = products[i].searchTerm || '';
if (!grouped[key]) grouped[key] = [];
grouped[key].push(products[i]);
}
var keys = Object.keys(grouped);
var multiGroup = keys.length > 1 || (keys.length === 1 && keys[0] !== '');
for (var g = 0; g < keys.length; g++) {
var term = keys[g];
if (multiGroup && term) container.append('<div class="group-label"><i class="fa fa-tag"></i> ' + term + '</div>');
var group = grouped[term];
for (var p = 0; p < group.length; p++) {
(function (product) {
var isMeasurable = product.isMeasurable;
var isReduced = product.isQuantityReduced;
var maxQty = product.stockQuantity;
var defaultQty = product.quantity;
var priceHtml = isMeasurable
? '<span class="measurable-badge"><i class="fa fa-balance-scale"></i> ' + qoStr.measurableBadge + '</span>'
: '<span class="pm-price">' + formatFt(product.unitPrice) + ' ' + qoStr.pricePerPiece + '</span>';
var warningHtml = isReduced
? '<div class="stock-warning-badge"><i class="fa fa-exclamation-triangle"></i> ' + qoStr.stockLimitedPrefix + ' ' + maxQty + ' ' + qoStr.stockLimitedSuffix + '</div>'
: '';
var card = $('<div>').addClass('product-card' + (isReduced ? ' has-warning' : ''));
card.html(
'<div class="pc-body">' +
'<div class="pc-name"><i class="fa fa-cube"></i> ' + product.name + '</div>' +
warningHtml +
'<div class="pc-meta">' +
'<span class="pc-stock' + (maxQty < 50 ? ' stock-low' : '') + '">' + qoStr.stockLabel + ' ' + maxQty + ' ' + qoStr.pieceUnit + '</span>' +
priceHtml +
'</div>' +
'</div>' +
'<div class="pc-actions">' +
'<div class="qty-stepper">' +
'<button type="button" class="qty-btn qty-minus" tabindex="-1"><i class="fa fa-minus"></i></button>' +
'<input type="number" class="qty-input" value="' + defaultQty + '" min="1" max="' + maxQty + '">' +
'<button type="button" class="qty-btn qty-plus" tabindex="-1"><i class="fa fa-plus"></i></button>' +
'</div>' +
'<button type="button" class="pc-add-btn" title="' + qoStr.addToCartTitle + '">' +
'<i class="fa fa-cart-arrow-down"></i>' +
'</button>' +
'</div>'
);
card.find('.qty-minus').click(function () {
var inp = $(this).siblings('.qty-input');
var val = parseInt(inp.val()) || 1;
if (val > 1) inp.val(val - 1);
});
card.find('.qty-plus').click(function () {
var inp = $(this).siblings('.qty-input');
var val = parseInt(inp.val()) || 1;
if (val < maxQty) inp.val(val + 1);
});
card.find('.qty-input').on('change blur', function () {
var val = parseInt($(this).val()) || 1;
val = Math.max(1, Math.min(maxQty, val));
$(this).val(val);
});
card.find('.pc-add-btn').click(function () {
var qty = parseInt(card.find('.qty-input').val()) || 1;
addToCart(product.id, qty, product.name, $(this));
});
container.append(card);
})(group[p]);
}
}
$('#productMatchesCard').show();
}
// ── Cart ──────────────────────────────────────────────────────────────────
function addToCart(productId, quantity, name, btnEl) {
btnEl.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i>');
$.ajax({
url: '@Url.Action("AddToCart", "QuickOrder")',
type: 'POST',
data: { productId: productId, quantity: quantity, __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val() },
success: function (result) {
if (result.success) {
btnEl.html('<i class="fa fa-check"></i>').addClass('added');
renderCart(result.cartItems);
showCartToast(name, quantity);
setTimeout(function () {
$('#searchInput').val('');
$('#transcribedCard').hide();
loadAllProducts();
}, 700);
} else {
alert(qoStr.errorPrefix + result.message);
btnEl.prop('disabled', false).html('<i class="fa fa-cart-arrow-down"></i>');
}
},
error: function () {
alert(qoStr.addToCartError);
btnEl.prop('disabled', false).html('<i class="fa fa-cart-arrow-down"></i>');
}
});
}
function loadCart() {
$.ajax({
url: '@Url.Action("GetCartItems", "QuickOrder")',
type: 'GET',
success: function (result) { if (result.success) renderCart(result.cartItems); }
});
}
function renderCart(items) {
var list = $('#cartItemsList').empty();
var count = items.length;
$('#cartItemCount').text(count);
if (count === 0) {
$('#cartEmptyState').show();
$('#cartItemsList, #cartTotalRow, #cartActions').hide();
return;
}
$('#cartEmptyState').hide();
$('#cartItemsList, #cartTotalRow, #cartActions').show();
var estimatedTotal = 0;
var hasMeasurable = false;
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (item.isMeasurable) hasMeasurable = true;
var lineTotal = item.isMeasurable ? null : (item.unitPrice * item.quantity);
if (lineTotal) estimatedTotal += lineTotal;
var lineTotalHtml = item.isMeasurable
? '<span class="measurable-badge-sm"><i class="fa fa-balance-scale"></i></span>'
: '<strong class="line-total">' + formatFt(lineTotal) + ' Ft</strong>';
var priceHtml = item.isMeasurable ? '' : '<span class="ci-price">' + formatFt(item.unitPrice) + ' ' + qoStr.pricePerPiece + '</span>';
list.append(
'<div class="cart-item">' +
'<div class="ci-name">' + item.name + '</div>' +
'<div class="ci-details">' +
'<span class="ci-qty">' + item.quantity + ' ' + qoStr.pieceUnit + '</span>' +
priceHtml + lineTotalHtml +
'</div>' +
'</div>'
);
}
$('#cartTotalAmount').text(formatFt(estimatedTotal) + ' Ft');
if (hasMeasurable) $('#cartTotalRow .cart-total-note').show();
else $('#cartTotalRow .cart-total-note').hide();
}
function showCartToast(name, qty) {
var toast = $('<div class="qo-toast"><i class="fa fa-check-circle"></i> <strong>' + name + '</strong> (' + qty + ' ' + qoStr.pieceUnit + ') ' + qoStr.addedToCart + '</div>');
$('body').append(toast);
setTimeout(function () { toast.addClass('show'); }, 10);
setTimeout(function () { toast.removeClass('show'); setTimeout(function () { toast.remove(); }, 400); }, 2500);
}
function formatFt(val) {
if (val === null || val === undefined) return '-';
return Math.round(val).toLocaleString('hu-HU');
}
</script>