650 lines
24 KiB
Plaintext
650 lines
24 KiB
Plaintext
@{
|
|
Layout = "_AdminLayout";
|
|
ViewBag.PageTitle = "Voice Order Creation";
|
|
}
|
|
|
|
<div class="content-header clearfix">
|
|
<h1 class="float-left">
|
|
<i class="fas fa-microphone"></i> Voice Order Creation
|
|
</h1>
|
|
</div>
|
|
|
|
<section class="content">
|
|
<div class="container-fluid">
|
|
|
|
<!-- Progress Steps -->
|
|
<div class="card card-default mb-3">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div id="step1Indicator" class="alert alert-info">
|
|
<i class="fas fa-user"></i> <strong>Step 1:</strong> Select Partner
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div id="step2Indicator" class="alert alert-secondary">
|
|
<i class="fas fa-box"></i> <strong>Step 2:</strong> Add Products
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 1: Partner Selection -->
|
|
<div id="step1Card" class="card card-primary">
|
|
<div class="card-header">
|
|
<h3 class="card-title">
|
|
<i class="fas fa-user"></i> Step 1: Select Partner
|
|
</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-center mb-4">
|
|
<h4 id="step1Prompt">🎤 Tell me the partner name</h4>
|
|
</div>
|
|
|
|
<!-- Voice Recording Button -->
|
|
<div class="text-center mb-4">
|
|
<button id="recordPartnerBtn" class="btn btn-lg btn-primary">
|
|
<i class="fas fa-microphone"></i> Click to Record Partner Name
|
|
</button>
|
|
<button id="stopPartnerBtn" class="btn btn-lg btn-danger" style="display: none;">
|
|
<i class="fas fa-stop"></i> Stop Recording
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Recording Status -->
|
|
<div id="partnerRecordingStatus" class="alert alert-info text-center" style="display: none;">
|
|
<i class="fas fa-spinner fa-spin"></i> <span id="partnerStatusText">Recording...</span>
|
|
</div>
|
|
|
|
<!-- Transcribed Text -->
|
|
<div id="partnerTranscribedCard" class="card card-secondary" style="display: none;">
|
|
<div class="card-header">
|
|
<h5 class="card-title"><i class="fas fa-comment"></i> You said:</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<p id="partnerTranscribedText" class="lead"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Partner Matches -->
|
|
<div id="partnerMatchesCard" class="card card-success" style="display: none;">
|
|
<div class="card-header">
|
|
<h5 class="card-title"><i class="fas fa-users"></i> Matching Partners:</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="partnerButtons" class="d-grid gap-2">
|
|
<!-- Partner buttons will be inserted here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selected Partner Display -->
|
|
<div id="selectedPartnerCard" class="card card-success" style="display: none;">
|
|
<div class="card-header">
|
|
<h5 class="card-title"><i class="fas fa-check-circle"></i> Selected Partner:</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<h4 id="selectedPartnerName"></h4>
|
|
<p id="selectedPartnerDetails" class="text-muted"></p>
|
|
<button id="changePartnerBtn" class="btn btn-warning">
|
|
<i class="fas fa-undo"></i> Change Partner
|
|
</button>
|
|
<button id="proceedToProductsBtn" class="btn btn-success">
|
|
<i class="fas fa-arrow-right"></i> Proceed to Add Products
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Product Selection -->
|
|
<div id="step2Card" class="card card-info" style="display: none;">
|
|
<div class="card-header">
|
|
<h3 class="card-title">
|
|
<i class="fas fa-box"></i> Step 2: Add Products
|
|
</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Selected Partner Summary -->
|
|
<div class="alert alert-success mb-3">
|
|
<strong><i class="fas fa-user"></i> Partner:</strong> <span id="partnerSummary"></span>
|
|
</div>
|
|
|
|
<div class="text-center mb-4">
|
|
<h4 id="step2Prompt">🎤 Tell me the products and quantities</h4>
|
|
<p class="text-muted">Example: "Narancs 100 kilogram, Alma 50 kilogram"</p>
|
|
</div>
|
|
|
|
<!-- Voice Recording Button -->
|
|
<div class="text-center mb-4">
|
|
<button id="recordProductBtn" class="btn btn-lg btn-primary">
|
|
<i class="fas fa-microphone"></i> Click to Record Products
|
|
</button>
|
|
<button id="stopProductBtn" class="btn btn-lg btn-danger" style="display: none;">
|
|
<i class="fas fa-stop"></i> Stop Recording
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Recording Status -->
|
|
<div id="productRecordingStatus" class="alert alert-info text-center" style="display: none;">
|
|
<i class="fas fa-spinner fa-spin"></i> <span id="productStatusText">Recording...</span>
|
|
</div>
|
|
|
|
<!-- Transcribed Text -->
|
|
<div id="productTranscribedCard" class="card card-secondary" style="display: none;">
|
|
<div class="card-header">
|
|
<h5 class="card-title"><i class="fas fa-comment"></i> You said:</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<p id="productTranscribedText" class="lead"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product Matches (for confirmation) -->
|
|
<div id="productMatchesCard" class="card card-warning" style="display: none;">
|
|
<div class="card-header">
|
|
<h5 class="card-title"><i class="fas fa-boxes"></i> Found Products - Click to Confirm:</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="productButtons" class="d-grid gap-2">
|
|
<!-- Product buttons will be inserted here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Current Order Items -->
|
|
<div id="orderItemsCard" class="card card-success" style="display: none;">
|
|
<div class="card-header">
|
|
<h5 class="card-title"><i class="fas fa-shopping-cart"></i> Current Order Items:</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<table class="table table-bordered" id="orderItemsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Product</th>
|
|
<th>Quantity</th>
|
|
<th>Unit Price</th>
|
|
<th>Total</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="orderItemsBody">
|
|
<!-- Order items will be inserted here -->
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th colspan="3" class="text-right">Order Total:</th>
|
|
<th id="orderTotalDisplay">0.00 Ft</th>
|
|
<th></th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<div class="text-center mt-3">
|
|
<button id="addMoreProductsBtn" class="btn btn-primary">
|
|
<i class="fas fa-microphone"></i> Add More Products
|
|
</button>
|
|
<button id="finishOrderBtn" class="btn btn-success btn-lg">
|
|
<i class="fas fa-check"></i> Finish & Create Order
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Creation Success -->
|
|
<div id="successCard" class="card card-success" style="display: none;">
|
|
<div class="card-header">
|
|
<h3 class="card-title">
|
|
<i class="fas fa-check-circle"></i> Order Created Successfully!
|
|
</h3>
|
|
</div>
|
|
<div class="card-body text-center">
|
|
<h3>Order #<span id="createdOrderId"></span> has been created!</h3>
|
|
<div class="mt-4">
|
|
<a id="viewOrderBtn" href="#" class="btn btn-primary btn-lg">
|
|
<i class="fas fa-eye"></i> View Order
|
|
</a>
|
|
<button id="createAnotherOrderBtn" class="btn btn-secondary btn-lg">
|
|
<i class="fas fa-plus"></i> Create Another Order
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<script>
|
|
// State management
|
|
let currentStep = 1;
|
|
let selectedPartnerId = null;
|
|
let selectedPartnerName = "";
|
|
let orderItems = [];
|
|
let mediaRecorder = null;
|
|
let audioChunks = [];
|
|
let orderTotal = 0;
|
|
|
|
$(document).ready(function() {
|
|
setupEventHandlers();
|
|
});
|
|
|
|
function setupEventHandlers() {
|
|
// Partner recording
|
|
$('#recordPartnerBtn').click(() => startRecording('partner'));
|
|
$('#stopPartnerBtn').click(() => stopRecording('partner'));
|
|
|
|
// Product recording
|
|
$('#recordProductBtn').click(() => startRecording('product'));
|
|
$('#stopProductBtn').click(() => stopRecording('product'));
|
|
|
|
// Navigation
|
|
$('#changePartnerBtn').click(resetPartnerSelection);
|
|
$('#proceedToProductsBtn').click(proceedToProducts);
|
|
$('#addMoreProductsBtn').click(addMoreProducts);
|
|
$('#finishOrderBtn').click(finishOrder);
|
|
$('#createAnotherOrderBtn').click(resetWizard);
|
|
}
|
|
|
|
async function startRecording(type) {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
mediaRecorder = new MediaRecorder(stream);
|
|
audioChunks = [];
|
|
|
|
mediaRecorder.ondataavailable = (event) => {
|
|
audioChunks.push(event.data);
|
|
};
|
|
|
|
mediaRecorder.onstop = () => {
|
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
|
processAudio(audioBlob, type);
|
|
stream.getTracks().forEach(track => track.stop());
|
|
};
|
|
|
|
mediaRecorder.start();
|
|
|
|
// Update UI
|
|
if (type === 'partner') {
|
|
$('#recordPartnerBtn').hide();
|
|
$('#stopPartnerBtn').show();
|
|
showStatus('partnerRecordingStatus', 'Recording... Speak now!');
|
|
} else {
|
|
$('#recordProductBtn').hide();
|
|
$('#stopProductBtn').show();
|
|
showStatus('productRecordingStatus', 'Recording... Speak now!');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error accessing microphone:', error);
|
|
alert('Could not access microphone. Please check permissions.');
|
|
}
|
|
}
|
|
|
|
function stopRecording(type) {
|
|
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
|
mediaRecorder.stop();
|
|
|
|
// Update UI
|
|
if (type === 'partner') {
|
|
$('#stopPartnerBtn').hide();
|
|
showStatus('partnerRecordingStatus', 'Processing audio...');
|
|
} else {
|
|
$('#stopProductBtn').hide();
|
|
showStatus('productRecordingStatus', 'Processing audio...');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function processAudio(audioBlob, type) {
|
|
const formData = new FormData();
|
|
formData.append('audioFile', audioBlob, 'recording.webm');
|
|
|
|
const endpoint = type === 'partner'
|
|
? '@Url.Action("TranscribeForPartner", "VoiceOrder")'
|
|
: '@Url.Action("TranscribeForProducts", "VoiceOrder")';
|
|
|
|
try {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val()
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
if (type === 'partner') {
|
|
handlePartnerTranscription(result);
|
|
} else {
|
|
handleProductTranscription(result);
|
|
}
|
|
} else {
|
|
alert('Error: ' + result.message);
|
|
resetRecordingUI(type);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error processing audio:', error);
|
|
alert('Error processing audio: ' + error.message);
|
|
resetRecordingUI(type);
|
|
}
|
|
}
|
|
|
|
function handlePartnerTranscription(result) {
|
|
$('#partnerRecordingStatus').hide();
|
|
$('#recordPartnerBtn').show();
|
|
|
|
// Show transcribed text
|
|
$('#partnerTranscribedText').text(result.transcription);
|
|
$('#partnerTranscribedCard').show();
|
|
|
|
// Show matching partners
|
|
if (result.partners && result.partners.length > 0) {
|
|
displayPartnerMatches(result.partners);
|
|
} else {
|
|
alert('No matching partners found. Please try again.');
|
|
}
|
|
}
|
|
|
|
function displayPartnerMatches(partners) {
|
|
const container = $('#partnerButtons');
|
|
container.empty();
|
|
|
|
partners.forEach(partner => {
|
|
const btn = $('<button>')
|
|
.addClass('btn btn-outline-primary btn-lg mb-2 text-left')
|
|
.css('width', '100%')
|
|
.html(`
|
|
<i class="fas fa-user"></i> <strong>${partner.label}</strong><br>
|
|
<small class="text-muted">ID: ${partner.value}</small>
|
|
`)
|
|
.click(() => selectPartner(partner));
|
|
|
|
container.append(btn);
|
|
});
|
|
|
|
$('#partnerMatchesCard').show();
|
|
}
|
|
|
|
function selectPartner(partner) {
|
|
selectedPartnerId = partner.value;
|
|
selectedPartnerName = partner.label;
|
|
|
|
$('#selectedPartnerName').text(partner.label);
|
|
$('#selectedPartnerDetails').text('Customer ID: ' + partner.value);
|
|
|
|
$('#partnerMatchesCard').hide();
|
|
$('#selectedPartnerCard').show();
|
|
}
|
|
|
|
function resetPartnerSelection() {
|
|
selectedPartnerId = null;
|
|
selectedPartnerName = "";
|
|
|
|
$('#partnerTranscribedCard').hide();
|
|
$('#partnerMatchesCard').hide();
|
|
$('#selectedPartnerCard').hide();
|
|
$('#recordPartnerBtn').show();
|
|
}
|
|
|
|
function proceedToProducts() {
|
|
currentStep = 2;
|
|
|
|
// Update step indicators
|
|
$('#step1Indicator').removeClass('alert-info').addClass('alert-success');
|
|
$('#step2Indicator').removeClass('alert-secondary').addClass('alert-info');
|
|
|
|
// Hide step 1, show step 2
|
|
$('#step1Card').hide();
|
|
$('#step2Card').show();
|
|
|
|
// Update partner summary
|
|
$('#partnerSummary').text(selectedPartnerName);
|
|
}
|
|
|
|
function handleProductTranscription(result) {
|
|
$('#productRecordingStatus').hide();
|
|
$('#recordProductBtn').show();
|
|
|
|
// Show transcribed text
|
|
$('#productTranscribedText').text(result.transcription);
|
|
$('#productTranscribedCard').show();
|
|
|
|
// Show matching products for confirmation
|
|
if (result.products && result.products.length > 0) {
|
|
displayProductMatches(result.products);
|
|
} else {
|
|
alert('No matching products found. Please try again.');
|
|
}
|
|
}
|
|
|
|
function displayProductMatches(products) {
|
|
const container = $('#productButtons');
|
|
container.empty();
|
|
|
|
products.forEach(product => {
|
|
const btn = $('<button>')
|
|
.addClass('btn btn-outline-success btn-lg mb-2 text-left')
|
|
.css('width', '100%')
|
|
.html(`
|
|
<i class="fas fa-box"></i> <strong>${product.name}</strong><br>
|
|
<small>Quantity: ${product.quantity} ${product.unit || 'kg'} | Price: ${product.price.toFixed(2)} Ft | Available: ${product.stockQuantity}</small>
|
|
`)
|
|
.click(() => addProductToOrder(product));
|
|
|
|
container.append(btn);
|
|
});
|
|
|
|
$('#productMatchesCard').show();
|
|
}
|
|
|
|
function addProductToOrder(product) {
|
|
// Check if product already exists in order
|
|
const existingIndex = orderItems.findIndex(item => item.id === product.id);
|
|
|
|
if (existingIndex >= 0) {
|
|
// Update quantity
|
|
orderItems[existingIndex].quantity += product.quantity;
|
|
} else {
|
|
// Add new item
|
|
orderItems.push({
|
|
id: product.id,
|
|
name: product.name,
|
|
sku: product.sku,
|
|
quantity: product.quantity,
|
|
price: product.price,
|
|
stockQuantity: product.stockQuantity
|
|
});
|
|
}
|
|
|
|
updateOrderItemsDisplay();
|
|
|
|
// Hide product matches after adding
|
|
$('#productMatchesCard').hide();
|
|
$('#productTranscribedCard').hide();
|
|
}
|
|
|
|
function updateOrderItemsDisplay() {
|
|
const tbody = $('#orderItemsBody');
|
|
tbody.empty();
|
|
orderTotal = 0;
|
|
|
|
orderItems.forEach((item, index) => {
|
|
const itemTotal = item.quantity * item.price;
|
|
orderTotal += itemTotal;
|
|
|
|
const row = $('<tr>').html(`
|
|
<td>${item.name}<br><small class="text-muted">SKU: ${item.sku}</small></td>
|
|
<td>
|
|
<input type="number" class="form-control" value="${item.quantity}"
|
|
onchange="updateQuantity(${index}, this.value)" min="1" max="${item.stockQuantity}">
|
|
</td>
|
|
<td>${item.price.toFixed(2)} Ft</td>
|
|
<td>${itemTotal.toFixed(2)} Ft</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-danger" onclick="removeItem(${index})">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
`);
|
|
tbody.append(row);
|
|
});
|
|
|
|
$('#orderTotalDisplay').text(orderTotal.toFixed(2) + ' Ft');
|
|
$('#orderItemsCard').show();
|
|
}
|
|
|
|
window.updateQuantity = function(index, newQuantity) {
|
|
orderItems[index].quantity = parseInt(newQuantity);
|
|
updateOrderItemsDisplay();
|
|
};
|
|
|
|
window.removeItem = function(index) {
|
|
orderItems.splice(index, 1);
|
|
updateOrderItemsDisplay();
|
|
|
|
if (orderItems.length === 0) {
|
|
$('#orderItemsCard').hide();
|
|
}
|
|
};
|
|
|
|
function addMoreProducts() {
|
|
$('#productTranscribedCard').hide();
|
|
$('#productMatchesCard').hide();
|
|
$('#recordProductBtn').show();
|
|
}
|
|
|
|
async function finishOrder() {
|
|
if (!selectedPartnerId) {
|
|
alert('No partner selected!');
|
|
return;
|
|
}
|
|
|
|
if (orderItems.length === 0) {
|
|
alert('No products in order!');
|
|
return;
|
|
}
|
|
|
|
$('#finishOrderBtn').prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Creating order...');
|
|
|
|
try {
|
|
const orderProductsJson = JSON.stringify(orderItems);
|
|
|
|
const response = await fetch('@Url.Action("Create", "CustomOrder")', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val()
|
|
},
|
|
body: new URLSearchParams({
|
|
customerId: selectedPartnerId,
|
|
orderProductsJson: orderProductsJson,
|
|
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
|
|
})
|
|
});
|
|
|
|
// Check if redirect happened (order created successfully)
|
|
if (response.redirected) {
|
|
// Extract order ID from redirect URL
|
|
const url = new URL(response.url);
|
|
const orderId = url.searchParams.get('id');
|
|
|
|
if (orderId) {
|
|
showSuccess(orderId);
|
|
} else {
|
|
// Fallback: redirect to the order edit page
|
|
window.location.href = response.url;
|
|
}
|
|
} else {
|
|
alert('Error creating order. Please try again.');
|
|
$('#finishOrderBtn').prop('disabled', false).html('<i class="fas fa-check"></i> Finish & Create Order');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating order:', error);
|
|
alert('Error creating order: ' + error.message);
|
|
$('#finishOrderBtn').prop('disabled', false).html('<i class="fas fa-check"></i> Finish & Create Order');
|
|
}
|
|
}
|
|
|
|
function showSuccess(orderId) {
|
|
$('#step2Card').hide();
|
|
$('#successCard').show();
|
|
$('#createdOrderId').text(orderId);
|
|
$('#viewOrderBtn').attr('href', '@Url.Action("Edit", "Order")?id=' + orderId);
|
|
}
|
|
|
|
function resetWizard() {
|
|
// Reset all state
|
|
currentStep = 1;
|
|
selectedPartnerId = null;
|
|
selectedPartnerName = "";
|
|
orderItems = [];
|
|
orderTotal = 0;
|
|
|
|
// Reset UI
|
|
$('#step1Indicator').removeClass('alert-success').addClass('alert-info');
|
|
$('#step2Indicator').removeClass('alert-info').addClass('alert-secondary');
|
|
|
|
$('#successCard').hide();
|
|
$('#step2Card').hide();
|
|
$('#step1Card').show();
|
|
|
|
$('#partnerTranscribedCard').hide();
|
|
$('#partnerMatchesCard').hide();
|
|
$('#selectedPartnerCard').hide();
|
|
$('#productTranscribedCard').hide();
|
|
$('#productMatchesCard').hide();
|
|
$('#orderItemsCard').hide();
|
|
|
|
$('#recordPartnerBtn').show();
|
|
}
|
|
|
|
function showStatus(elementId, message) {
|
|
$(`#${elementId}`).find('span').text(message);
|
|
$(`#${elementId}`).show();
|
|
}
|
|
|
|
function resetRecordingUI(type) {
|
|
if (type === 'partner') {
|
|
$('#partnerRecordingStatus').hide();
|
|
$('#recordPartnerBtn').show();
|
|
$('#stopPartnerBtn').hide();
|
|
} else {
|
|
$('#productRecordingStatus').hide();
|
|
$('#recordProductBtn').show();
|
|
$('#stopProductBtn').hide();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.d-grid {
|
|
display: grid;
|
|
}
|
|
|
|
.gap-2 {
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
#step1Indicator, #step2Indicator {
|
|
font-size: 1.1rem;
|
|
text-align: center;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.btn-lg {
|
|
padding: 1rem 2rem;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.card-body h4 {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
</style>
|
|
|
|
@* Anti-forgery token *@
|
|
@Html.AntiForgeryToken()
|