567 lines
28 KiB
Plaintext
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>
|