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');