Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Views/CustomerPreorder/List.cshtml

504 lines
22 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@using FruitBank.Common.Enums
@using Nop.Plugin.Misc.FruitBankPlugin.Controllers
@model List<CustomerPreOrderController.CustomerPreOrderRow>
@{
Layout = "_ColumnsTwo";
ViewBag.Title = "ElĹrendelĂ©seim";
}
<link rel="stylesheet" href="~/Plugins/Misc.FruitBankPlugin/css/quick-order.css" />
<div class="page account-page my-preorders-page">
<div class="page-title">
<h1>ElĹrendelĂ©seim</h1>
</div>
<div class="page-body">
@if (!Model.Any())
{
<div class="no-data">
<p>MĂ©g nem adtál le elĹrendelĂ©st.</p>
<a href="@Url.Action("Index", "Order")" class="button-1">Rendelés indítása</a>
</div>
}
else
{
<!-- ── Search bar ────────────────────────────────────────────── -->
<div class="po-search-wrap">
<div class="search-input-group">
<button type="button" id="poRecordBtn" class="mic-btn" title="Hangfelvétel indítása">
<i class="fa fa-microphone"></i>
</button>
<button type="button" id="poStopBtn" class="mic-btn mic-btn-recording" style="display:none;" title="Leállítás">
<i class="fa fa-stop"></i>
</button>
<input type="text" id="poSearchInput" class="qo-input"
placeholder="Keress termĂ©kre az elĹrendelĂ©seid között..."
onkeypress="if(event.key==='Enter') filterPreorders()" />
<button type="button" class="qo-search-btn" onclick="filterPreorders()">
<i class="fa fa-search"></i> Keresés
</button>
</div>
<div id="poRecordingStatus" class="recording-status-bar" style="display:none;">
<span id="poStatusText">Figyelés...</span>
<div class="volume-bar-container">
<div class="volume-bar volume-bar-silent"></div>
</div>
</div>
<div id="poSearchNoResults" class="po-search-empty" style="display:none;">
<i class="fa fa-search"></i> Nincs találat a keresési feltételre.
<button type="button" class="po-clear-btn" onclick="clearFilter()">
<i class="fa fa-times"></i> Mutasd az összeset
</button>
</div>
</div>
<!-- ── Preorder cards ────────────────────────────────────────── -->
<div id="poCardList">
@foreach (var preorder in Model)
{
var statusClass = preorder.Status switch
{
PreOrderStatus.Confirmed => "po-status-confirmed",
PreOrderStatus.PartiallyFulfilled => "po-status-partial",
PreOrderStatus.Cancelled => "po-status-cancelled",
_ => "po-status-pending"
};
var statusLabel = preorder.Status switch
{
PreOrderStatus.Confirmed => "MegerĹ­tve",
PreOrderStatus.PartiallyFulfilled => "Részben teljesítve",
PreOrderStatus.Cancelled => "Törölve / Lejárt",
_ => "FĂĽggĹben"
};
// Build searchable text for client-side filtering
var searchText = string.Join(" ",
preorder.Items.Select(i => i.ProductName)
.Append(preorder.CustomerNote ?? "")
).ToLowerInvariant();
<div class="po-customer-card" data-search="@searchText">
<div class="po-card-header">
<div class="po-card-meta">
<span class="po-card-id">#@preorder.PreOrderId elĹrendelĂ©s</span>
<span class="po-card-date">
<i class="fa fa-calendar"></i>
Kért szállítás: <strong>@preorder.DateOfReceipt.ToLocalTime().ToString("yyyy. MM. dd. HH:mm")</strong>
</span>
<span class="po-card-created">
Leadva: @preorder.CreatedOnUtc.ToLocalTime().ToString("yyyy. MM. dd.")
</span>
</div>
<div class="po-card-status-wrap">
<span class="po-status-badge @statusClass">@statusLabel</span>
@if (preorder.OrderId.HasValue)
{
<a href="@Url.RouteUrl("OrderDetails", new { orderId = preorder.OrderId })"
class="po-order-link">
<i class="fa fa-external-link"></i> Rendelés #@preorder.OrderId
</a>
}
</div>
</div>
@if (!string.IsNullOrWhiteSpace(preorder.CustomerNote))
{
<div class="po-card-note">
<i class="fa fa-comment-o"></i>
@preorder.CustomerNote
</div>
}
<div class="po-card-items">
<table class="po-items-table">
<thead>
<tr>
<th>Termék</th>
<th class="text-center">Kérve</th>
<th class="text-center">TeljesĂ­tve</th>
<th class="text-right">Egységár</th>
<th class="text-center po-status-col">Állapot</th>
</tr>
</thead>
<tbody>
@foreach (var item in preorder.Items)
{
var itemStatusLabel = item.Status switch
{
PreOrderItemStatus.Fulfilled => "âś“ TeljesĂ­tve",
PreOrderItemStatus.PartiallyFulfilled => "◑ Részben",
PreOrderItemStatus.Dropped => "âś• Ejtve",
_ => "⏳ Vár"
};
var itemStatusClass = item.Status switch
{
PreOrderItemStatus.Fulfilled => "item-fulfilled",
PreOrderItemStatus.PartiallyFulfilled => "item-partial",
PreOrderItemStatus.Dropped => "item-dropped",
_ => "item-pending"
};
var unitPrice = item.IsMeasurable
? "Súlymérés"
: item.UnitPriceInclTax.ToString("N0") + " Ft/db";
<tr class="@itemStatusClass">
<td>
@item.ProductName
@if (item.IsMeasurable)
{
<span class="measurable-tag" title="Súlymérést igényel">⚖️</span>
}
</td>
<td class="text-center">@item.RequestedQuantity db</td>
<td class="text-center">@item.FulfilledQuantity db</td>
<td class="text-right">@unitPrice</td>
<td class="text-center">
<span class="item-status-label @itemStatusClass">@itemStatusLabel</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
}
</div>
</div>
@Html.AntiForgeryToken()
<style>
/* ── Search bar ───────────────────────────────────────────────────────── */
.po-search-wrap {
margin-bottom: 20px;
}
.po-search-empty {
margin-top: 10px;
padding: 12px 16px;
background: #fff8ee;
border: 1px solid #f4c87a;
border-radius: 8px;
font-size: 13px;
color: #7a4200;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.po-clear-btn {
background: none;
border: 1px solid #c87500;
border-radius: 4px;
padding: 2px 10px;
font-size: 12px;
color: #c87500;
cursor: pointer;
margin-left: auto;
}
.po-clear-btn:hover { background: #fff3cd; }
/* ── Page ─────────────────────────────────────────────────────────────── */
.my-preorders-page .page-title h1 {
font-size: 24px;
color: #1a3c22;
font-weight: 700;
}
.no-data {
text-align: center;
padding: 48px 20px;
color: #6b7c6e;
}
.no-data p { margin-bottom: 16px; font-size: 15px; }
/* ── PreOrder card ────────────────────────────────────────────────────── */
.po-customer-card {
background: #fff;
border: 1px solid #dde8da;
border-radius: 10px;
margin-bottom: 20px;
overflow: hidden;
transition: opacity 0.2s;
}
.po-customer-card.po-hidden {
display: none;
}
.po-card-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 16px 20px;
background: #f5f7f2;
border-bottom: 1px solid #dde8da;
}
.po-card-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
font-size: 13px;
color: #6b7c6e;
}
.po-card-id {
font-weight: 700;
color: #1a3c22;
font-size: 14px;
}
.po-card-date strong { color: #1a3c22; }
.po-card-status-wrap {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.po-status-badge {
display: inline-block;
padding: 3px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
}
.po-status-pending { background: #fff3cd; color: #856404; }
.po-status-confirmed { background: #d4edda; color: #155724; }
.po-status-partial { background: #fff8ee; color: #c87500; }
.po-status-cancelled { background: #f8d7da; color: #721c24; }
.po-order-link {
font-size: 12px;
font-weight: 600;
color: #2d7a3a;
text-decoration: none;
border: 1px solid #2d7a3a;
border-radius: 4px;
padding: 2px 10px;
}
.po-order-link:hover { background: #2d7a3a; color: #fff; }
.po-card-note {
padding: 10px 20px;
font-size: 13px;
color: #6b7c6e;
background: #fffdf7;
border-bottom: 1px solid #dde8da;
}
.po-card-note .fa { margin-right: 6px; color: #f4a236; }
.po-card-items { padding: 0; }
.po-items-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.po-items-table th {
padding: 8px 14px;
background: #f0f4ee;
color: #1a3c22;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .5px;
border-bottom: 1px solid #dde8da;
}
.po-items-table td {
padding: 10px 14px;
border-bottom: 1px solid #f0f4ee;
color: #2c3e2e;
vertical-align: middle;
}
.po-items-table tr:last-child td { border-bottom: none; }
.po-items-table tr.item-fulfilled { background: #f6fbf4; }
.po-items-table tr.item-partial { background: #fffbf0; }
.po-items-table tr.item-dropped { background: #fdf6f6; color: #999; }
.item-status-label {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.item-status-label.item-fulfilled { background: #d4edda; color: #155724; }
.item-status-label.item-partial { background: #fff8ee; color: #c87500; }
.item-status-label.item-dropped { background: #f8d7da; color: #721c24; }
.item-status-label.item-pending { background: #fff3cd; color: #856404; }
.measurable-tag { margin-left: 4px; font-size: 13px; }
.text-center { text-align: center; }
.text-right { text-align: right; }
@@media (max-width: 600px) {
.po-status-col { display: none; }
.po-items-table th:last-child,
.po-items-table td:last-child { display: none; }
}
</style>
<script asp-location="Footer">
// ── Filter ────────────────────────────────────────────────────────────────
function filterPreorders() {
var term = ($('#poSearchInput').val() || '').trim().toLowerCase();
if (!term) { clearFilter(); return; }
var visible = 0;
$('.po-customer-card').each(function () {
var searchData = ($(this).attr('data-search') || '').toLowerCase();
if (searchData.indexOf(term) !== -1) {
$(this).removeClass('po-hidden'); visible++;
} else {
$(this).addClass('po-hidden');
}
});
if (visible === 0) {
$('#poSearchNoResults').show();
} else {
$('#poSearchNoResults').hide();
}
}
function clearFilter() {
$('#poSearchInput').val('');
$('.po-customer-card').removeClass('po-hidden');
$('#poSearchNoResults').hide();
}
// ── Voice input ───────────────────────────────────────────────────────────
var poMediaRecorder = null, poAudioChunks = [], poIsRecording = false;
var poAudioContext = null, poAnalyser = null, poVolumeInterval = null;
var poRecordingStart = null, poBaselineNoise = -60, poVolumeHistory = [];
var VAD = { silenceDuration:1500, minRecordingTime:800, volumeCheckInterval:100,
calibrationTime:500, noiseGateOffset:15, volumeHistorySize:10 };
function getSupportedMimeType() {
var types = ['audio/webm','audio/webm;codecs=opus','audio/ogg;codecs=opus','audio/mp4'];
for (var i = 0; i < types.length; i++) if (MediaRecorder.isTypeSupported(types[i])) return types[i];
return 'audio/webm';
}
$('#poRecordBtn').click(function () {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('A böngészŠnem támogatja a hangfelvételt.'); return;
}
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
poAudioContext = new (window.AudioContext || window.webkitAudioContext)();
poAnalyser = poAudioContext.createAnalyser();
poAudioContext.createMediaStreamSource(stream).connect(poAnalyser);
poAnalyser.fftSize = 512;
var mimeType = getSupportedMimeType();
poMediaRecorder = new MediaRecorder(stream, { mimeType: mimeType });
poAudioChunks = []; poRecordingStart = Date.now(); poIsRecording = true;
poMediaRecorder.addEventListener('dataavailable', function (e) { poAudioChunks.push(e.data); });
poMediaRecorder.addEventListener('stop', function () {
var blob = new Blob(poAudioChunks, { type: mimeType });
stream.getTracks().forEach(function (t) { t.stop(); });
if (poAudioContext) { poAudioContext.close(); poAudioContext = null; }
poAnalyser = null; poIsRecording = false;
if (blob.size === 0) { resetPoRecordingUI(); return; }
transcribeForFilter(blob, mimeType);
});
poMediaRecorder.start();
$('#poRecordBtn').hide(); $('#poStopBtn').show();
$('#poSearchInput').attr('placeholder', 'Figyelés... (mondd a keresett termék nevét)');
poShowStatus('Figyelés...'); startPoVAD();
}).catch(function (err) { alert('Nem sikerült a mikrofon elérése: ' + err.message); });
});
$('#poStopBtn').click(function () { stopPoRecording(); });
function startPoVAD() {
var bufferLength = poAnalyser.frequencyBinCount, dataArray = new Uint8Array(bufferLength);
if (poVolumeInterval) clearInterval(poVolumeInterval);
var silentChecks = 0, silentNeeded = Math.ceil(VAD.silenceDuration / VAD.volumeCheckInterval);
var calibrated = false, calibSamples = []; poVolumeHistory = [];
poVolumeInterval = setInterval(function () {
if (!poIsRecording || !poAnalyser) { clearInterval(poVolumeInterval); return; }
poAnalyser.getByteFrequencyData(dataArray);
var sum = 0; for (var i = 0; i < bufferLength; i++) sum += dataArray[i];
var volume = 20 * Math.log10((sum / bufferLength) / 255);
var elapsed = Date.now() - poRecordingStart;
if (!calibrated && elapsed < VAD.calibrationTime) { calibSamples.push(volume); return; }
if (!calibrated && calibSamples.length > 0) {
var tot = 0; for (var j = 0; j < calibSamples.length; j++) tot += calibSamples[j];
poBaselineNoise = tot / calibSamples.length; calibrated = true;
}
poVolumeHistory.push(volume);
if (poVolumeHistory.length > VAD.volumeHistorySize) poVolumeHistory.shift();
var vs = 0; for (var k = 0; k < poVolumeHistory.length; k++) vs += poVolumeHistory[k];
var avgVol = vs / poVolumeHistory.length;
var threshold = poBaselineNoise + VAD.noiseGateOffset;
var norm = Math.max(0, Math.min(100, ((volume - threshold + 10) / 40) * 100));
var text = 'Figyelés...', cls = 'volume-bar-silent';
if (norm > 60) { text = 'Hangos Ă©s Ă©rthetĹ'; cls = 'volume-bar-high'; }
else if (norm > 30) { text = 'Beszél...'; cls = 'volume-bar-medium'; }
else if (norm > 10) { text = 'Hangosabban!'; cls = 'volume-bar-low'; }
$('#poStatusText').text(text);
$('#poRecordingStatus .volume-bar').removeClass('volume-bar-low volume-bar-medium volume-bar-high volume-bar-silent').addClass(cls).css('width', norm + '%');
if (elapsed < VAD.minRecordingTime) return;
if (avgVol < threshold) { silentChecks++; if (silentChecks >= silentNeeded) { clearInterval(poVolumeInterval); stopPoRecording(); } }
else { silentChecks = 0; }
}, VAD.volumeCheckInterval);
}
function stopPoRecording() {
if (poVolumeInterval) { clearInterval(poVolumeInterval); poVolumeInterval = null; }
if (poMediaRecorder && poMediaRecorder.state !== 'inactive') {
poShowStatus('Feldolgozás...'); poMediaRecorder.stop();
}
}
function transcribeForFilter(blob, mimeType) {
var formData = new FormData();
formData.append('audioFile', blob, 'recording.webm');
formData.append('__RequestVerificationToken', $('input[name="__RequestVerificationToken"]').val());
$.ajax({
url : '@Url.Action("TranscribeOnly", "Order")',
type : 'POST',
data : formData,
processData: false,
contentType: false,
success : function (r) {
resetPoRecordingUI();
if (r.success && r.transcription) {
$('#poSearchInput').val(r.transcription);
filterPreorders();
}
},
error: function () { resetPoRecordingUI(); }
});
}
function resetPoRecordingUI() {
$('#poRecordingStatus').hide();
$('#poRecordBtn').show(); $('#poStopBtn').hide();
$('#poSearchInput').attr('placeholder', 'Keress termĂ©kre az elĹrendelĂ©seid között...');
}
function poShowStatus(msg) {
$('#poStatusText').text(msg); $('#poRecordingStatus').show();
}
</script>