504 lines
22 KiB
Plaintext
504 lines
22 KiB
Plaintext
@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Ĺ‘sĂ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>
|