New Quotator and some major changes to pricing admin

This commit is contained in:
Mikhail Chusavitin
2026-01-26 18:30:45 +03:00
parent a93644131c
commit d7d6e9d62c
24 changed files with 565 additions and 112 deletions

View File

@@ -29,11 +29,23 @@
</div>
</div>
<!-- Search (only for components) -->
<!-- Search and sort (only for components) -->
<div id="search-bar" class="mb-4 hidden">
<input type="text" id="search-input" placeholder="Поиск по артикулу..."
class="w-full px-3 py-2 border rounded"
onkeyup="debounceSearch()">
<div class="flex gap-4 items-center">
<input type="text" id="search-input" placeholder="Поиск по артикулу..."
class="flex-1 px-3 py-2 border rounded"
onkeyup="debounceSearch()">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">Сортировка:</span>
<select id="sort-field" class="px-2 py-1 border rounded text-sm" onchange="changeSort()">
<option value="lot_name">Артикул</option>
<option value="popularity_score">Популярность</option>
<option value="quote_count">Кол-во котировок</option>
<option value="current_price">Цена</option>
</select>
<button onclick="toggleSortDir()" id="sort-dir-btn" class="px-2 py-1 border rounded text-sm"></button>
</div>
</div>
</div>
<div id="tab-content">
@@ -128,6 +140,8 @@ let perPage = 50;
let searchTimeout = null;
let currentSearch = '';
let componentsCache = [];
let sortField = 'lot_name';
let sortDir = 'asc';
async function loadTab(tab) {
currentTab = tab;
@@ -235,20 +249,49 @@ function renderComponents(components, total) {
return;
}
// Sort components locally
const sorted = [...components].sort((a, b) => {
let aVal, bVal;
switch (sortField) {
case 'popularity_score':
aVal = a.popularity_score || 0;
bVal = b.popularity_score || 0;
break;
case 'quote_count':
aVal = a.quote_count || 0;
bVal = b.quote_count || 0;
break;
case 'current_price':
aVal = a.current_price || 0;
bVal = b.current_price || 0;
break;
default:
aVal = a.lot_name || '';
bVal = b.lot_name || '';
}
if (sortDir === 'asc') {
return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
} else {
return aVal < bVal ? 1 : aVal > bVal ? -1 : 0;
}
});
let html = '<div class="overflow-x-auto"><table class="w-full"><thead class="bg-gray-50"><tr>';
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Артикул</th>';
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Категория</th>';
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>';
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Кол-во цен</th>';
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Популярность</th>';
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Кол-во котировок</th>';
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Цена</th>';
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Настройки</th>';
html += '</tr></thead><tbody class="divide-y">';
components.forEach((c, idx) => {
sorted.forEach((c, idx) => {
const price = c.current_price ? '$' + parseFloat(c.current_price).toLocaleString('en-US', {minimumFractionDigits: 2}) : '—';
const category = c.category ? c.category.code : '—';
const desc = c.lot && c.lot.lot_description ? c.lot.lot_description : '—';
const quoteCount = c.quote_count || 0;
const popularity = c.popularity_score ? c.popularity_score.toFixed(2) : '0.00';
// Build settings summary
let settings = [];
@@ -268,10 +311,14 @@ function renderComponents(components, total) {
settings.push('РУЧН');
}
html += '<tr class="hover:bg-gray-50 cursor-pointer" onclick="openModal(' + idx + ')">';
// Find original index in componentsCache
const origIdx = componentsCache.findIndex(x => x.lot_name === c.lot_name);
html += '<tr class="hover:bg-gray-50 cursor-pointer" onclick="openModal(' + origIdx + ')">';
html += '<td class="px-3 py-2 text-sm font-mono">' + escapeHtml(c.lot_name) + '</td>';
html += '<td class="px-3 py-2 text-sm">' + escapeHtml(category) + '</td>';
html += '<td class="px-3 py-2 text-sm text-gray-500 max-w-xs truncate">' + escapeHtml(desc) + '</td>';
html += '<td class="px-3 py-2 text-sm text-right">' + popularity + '</td>';
html += '<td class="px-3 py-2 text-sm text-right">' + quoteCount + '</td>';
html += '<td class="px-3 py-2 text-sm text-right font-medium">' + price + '</td>';
html += '<td class="px-3 py-2 text-sm"><span class="text-xs bg-gray-100 px-2 py-1 rounded">' + settings.join(' | ') + '</span></td>';
@@ -535,6 +582,17 @@ document.getElementById('price-modal').addEventListener('click', function(e) {
}
});
function changeSort() {
sortField = document.getElementById('sort-field').value;
renderComponents(componentsCache, componentsCache.length);
}
function toggleSortDir() {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
document.getElementById('sort-dir-btn').textContent = sortDir === 'asc' ? '↑' : '↓';
renderComponents(componentsCache, componentsCache.length);
}
document.addEventListener('DOMContentLoaded', () => {
loadTab('alerts');

View File

@@ -72,7 +72,7 @@
</div>
<!-- Autocomplete dropdown (shared) -->
<div id="autocomplete-dropdown" class="hidden absolute z-50 bg-white border rounded-lg shadow-lg max-h-60 overflow-y-auto w-96"></div>
<div id="autocomplete-dropdown" class="hidden absolute z-50 bg-white border rounded-lg shadow-lg max-h-96 overflow-y-auto w-96"></div>
<style>
.autocomplete-item {
@@ -435,7 +435,13 @@ function filterAutocomplete(category, search) {
if (!c.current_price) return false;
const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
return text.includes(searchLower);
}).slice(0, 50);
})
.sort((a, b) => {
// Sort by popularity_score desc, then by lot_name
const popDiff = (b.popularity_score || 0) - (a.popularity_score || 0);
if (popDiff !== 0) return popDiff;
return a.lot_name.localeCompare(b.lot_name);
});
renderAutocomplete();
}
@@ -453,9 +459,12 @@ function renderAutocomplete() {
dropdown.style.left = rect.left + 'px';
dropdown.style.width = Math.max(rect.width, 400) + 'px';
// Use different select function based on mode (single vs multi)
const selectFn = autocompleteCategory ? 'selectAutocompleteItem' : 'selectAutocompleteItemMulti';
dropdown.innerHTML = autocompleteFiltered.map((comp, idx) => `
<div class="autocomplete-item ${idx === autocompleteIndex ? 'selected' : ''}"
onmousedown="selectAutocompleteItem(${idx})">
onmousedown="${selectFn}(${idx})">
<div class="font-mono text-sm">${escapeHtml(comp.lot_name)}</div>
<div class="text-xs text-gray-500 truncate">${escapeHtml(comp.description || '')}</div>
</div>
@@ -535,7 +544,13 @@ function filterAutocompleteMulti(search) {
if (addedLots.has(c.lot_name)) return false;
const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
return text.includes(searchLower);
}).slice(0, 50);
})
.sort((a, b) => {
// Sort by popularity_score desc, then by lot_name
const popDiff = (b.popularity_score || 0) - (a.popularity_score || 0);
if (popDiff !== 0) return popDiff;
return a.lot_name.localeCompare(b.lot_name);
});
renderAutocomplete();
}