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

745 lines
37 KiB
Plaintext

@using System.Text.Encodings.Web
@{
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">
<!-- ── STEP 1: Delivery date + time picker ───────────────────────────── -->
<div id="deliveryStep" class="qo-delivery-step">
<div class="ds-header">
<i class="fa fa-calendar"></i>
<div>
<div class="ds-title">@T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.Title")</div>
<div class="ds-subtitle">@T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.Subtitle")</div>
</div>
</div>
<div class="ds-body">
<div class="ds-section-label">@T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.DayLabel")</div>
<div class="ds-day-buttons" id="dayButtons"></div>
<div class="ds-section-label" style="margin-top:20px;">
@T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.TimeLabel")
</div>
<div class="ds-time-wrapper">
<input type="time" id="deliveryTimePicker" class="ds-time-input" value="08:00" min="05:00" max="22:00" />
<span class="ds-time-hint">@T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.TimeHint")</span>
</div>
</div>
<div class="ds-footer">
<button type="button" class="ds-confirm-btn" id="deliveryConfirmBtn" disabled>
<i class="fa fa-arrow-right"></i> @T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.ConfirmButton")
</button>
</div>
</div>
<!-- ── Delivery chip (collapsed state) ──────────────────────────────── -->
<div id="deliveryChip" class="qo-delivery-chip" style="display:none;">
<i class="fa fa-calendar-check-o"></i>
<span class="dc-label">@T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.ChangeLabel")</span>
<strong id="deliveryChipText"></strong>
<button type="button" class="dc-change-btn" id="deliveryChangeBtn">
<i class="fa fa-pencil"></i> @T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.ChangeButton")
</button>
</div>
<!-- ── Search + layout (locked until delivery confirmed) ────────────── -->
<div id="mainContent" style="display:none;">
<!-- 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>
<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><!-- /#mainContent -->
</div><!-- /.quick-order-page -->
@Html.AntiForgeryToken()
@* JS string bundle *@
<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))',
dsToday: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.Today").Text))',
dsTomorrow: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.Tomorrow").Text))',
dsSaving: '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.Saving").Text))',
huDayNames: ['vas\u00e1rnap','h\u00e9tf\u0151','kedd','szerda','cs\u00fct\u00f6rt\u00f6k','p\u00e9ntek','szombat']
};
</script>
<script asp-location="Footer">
// ── State ─────────────────────────────────────────────────────────────────
var selectedDeliveryDate = null; // ISO date string e.g. "2026-04-16"
var selectedDeliveryTime = null; // e.g. "08:00"
var selectedDayLabel = null; // human-readable day label for chip
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
};
// ── Init ──────────────────────────────────────────────────────────────────
$(document).ready(function () {
renderDayButtons();
$(document).on('click', '.ds-day-btn', function () {
$('.ds-day-btn').removeClass('selected');
$(this).addClass('selected');
selectedDeliveryDate = $(this).data('date');
selectedDayLabel = $(this).data('label');
checkDeliveryReady();
});
// Time picker: any valid time enables the confirm button
$('#deliveryTimePicker').on('input change', function () {
selectedDeliveryTime = $(this).val() || null;
checkDeliveryReady();
});
// Initialise with the default value already set in the input
selectedDeliveryTime = $('#deliveryTimePicker').val() || null;
$('#deliveryConfirmBtn').click(confirmDelivery);
$('#deliveryChangeBtn').click(function () {
$('#deliveryChip').hide();
$('#mainContent').hide();
$('#deliveryStep').show();
});
$('#recordBtn').click(startRecording);
$('#stopBtn').click(function () { stopRecording(false); });
loadCart();
// ── Restore previously saved delivery datetime (e.g. new tab / page refresh) ──
$.ajax({
url: '@Url.Action("GetDeliveryDateTime", "QuickOrder")',
type: 'GET',
success: function (result) {
if (!result.success || !result.hasValue) return;
// Restore state variables
selectedDeliveryDate = result.date; // e.g. "2026-04-17"
selectedDeliveryTime = result.time; // e.g. "08:00"
// Mark the correct day button as selected
var $btn = $('.ds-day-btn[data-date="' + result.date + '"]');
if ($btn.length) {
$btn.addClass('selected');
selectedDayLabel = $btn.data('label');
} else {
// The saved date is beyond the 7-day window shown — just use the date string
selectedDayLabel = result.date;
}
// Restore the time picker value
$('#deliveryTimePicker').val(result.time);
// Skip the step and go straight to the product list
var chipText = selectedDayLabel + ' \u2014 ' + result.time;
$('#deliveryChipText').text(chipText);
$('#deliveryStep').hide();
$('#deliveryChip').show();
$('#mainContent').show();
loadAllProducts();
}
// On error: silently leave the step visible — user picks again
});
});
// ── Delivery step ─────────────────────────────────────────────────────────
function renderDayButtons() {
var container = $('#dayButtons').empty();
var today = new Date();
for (var i = 0; i < 7; i++) {
var d = new Date(today);
d.setDate(today.getDate() + i);
var iso = d.toISOString().split('T')[0];
var dayName;
if (i === 0) dayName = qoStr.dsToday;
else if (i === 1) dayName = qoStr.dsTomorrow;
else dayName = qoStr.huDayNames[d.getDay()];
var dateStr = (d.getMonth() + 1) + '. ' + d.getDate() + '.';
var btn = $('<button type="button" class="ds-day-btn">')
.attr('data-date', iso)
.attr('data-label', dayName + ' ' + dateStr)
.html('<span class="ds-day-name">' + dayName + '</span><span class="ds-day-date">' + dateStr + '</span>');
container.append(btn);
}
}
function checkDeliveryReady() {
$('#deliveryConfirmBtn').prop('disabled', !(selectedDeliveryDate && selectedDeliveryTime));
}
function confirmDelivery() {
var btn = $('#deliveryConfirmBtn');
btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> ' + qoStr.dsSaving);
// Combine date + time into ISO datetime string e.g. "2026-04-16T08:00"
var deliveryDateTime = selectedDeliveryDate + 'T' + selectedDeliveryTime;
$.ajax({
url: '@Url.Action("SetDeliveryDateTime", "QuickOrder")',
type: 'POST',
data: {
deliveryDateTime: deliveryDateTime,
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
},
success: function (result) {
if (!result.success) {
alert(qoStr.errorPrefix + (result.message || ''));
btn.prop('disabled', false).html('<i class="fa fa-arrow-right"></i> @Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.ConfirmButton").Text))');
return;
}
var chipText = selectedDayLabel + ' \u2014 ' + selectedDeliveryTime;
$('#deliveryChipText').text(chipText);
$('#deliveryStep').hide();
$('#deliveryChip').show();
$('#mainContent').show();
loadAllProducts();
},
error: function () {
alert(qoStr.errorPrefix);
btn.prop('disabled', false).html('<i class="fa fa-arrow-right"></i> @Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.QuickOrder.DeliveryStep.ConfirmButton").Text))');
}
});
}
// ── Product list ──────────────────────────────────────────────────────────
function loadAllProducts() {
$('#transcribedCard').hide();
$('#noResultsCard').hide();
$('#productMatchesCard').hide();
$('#productsLoadingState').show();
$('#matchesLabelText').text(qoStr.allProducts);
$.ajax({
url: '@Url.Action("GetAllProducts", "QuickOrder")',
type: 'GET',
data: { deliveryDate: selectedDeliveryDate, deliveryTime: selectedDeliveryTime },
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('deliveryDate', selectedDeliveryDate || '');
formData.append('deliveryTime', selectedDeliveryTime || '');
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,
deliveryDate: selectedDeliveryDate,
deliveryTime: selectedDeliveryTime,
__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>