Улучшения управления ценами и конфигурациями

- Добавлено отображение последней полученной цены в окне настройки цены
- Добавлен функционал переименования конфигураций (PATCH /api/configs/:uuid/rename)
- Изменён формат имени файла при экспорте: "YYYY-MM-DD NAME SPEC.ext"
- Исправлена сортировка компонентов: перенесена на сервер для корректной работы с пагинацией
- Добавлен расчёт popularity_score на основе котировок из lot_log
- Исправлена потеря настроек (метод, период, коэффициент) при пересчёте цен

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-01-27 11:39:12 +03:00
parent d7d6e9d62c
commit 7ded78f2c3
9 changed files with 269 additions and 73 deletions

View File

@@ -113,6 +113,8 @@
<div class="bg-gray-50 p-3 rounded space-y-2">
<div class="text-sm font-medium text-gray-700 mb-2">Расчёт цены</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div class="text-gray-600">Последняя цена:</div>
<div id="modal-last-price" class="font-medium text-right"></div>
<div class="text-gray-600">Медиана (всё время):</div>
<div id="modal-median-all" class="font-medium text-right"></div>
<div class="text-gray-600">Текущая цена:</div>
@@ -180,6 +182,12 @@ async function loadData() {
if (currentSearch) {
url += '&search=' + encodeURIComponent(currentSearch);
}
if (sortField) {
url += '&sort=' + encodeURIComponent(sortField);
}
if (sortDir) {
url += '&dir=' + encodeURIComponent(sortDir);
}
const resp = await fetch(url, {
headers: {'Authorization': 'Bearer ' + token}
});
@@ -249,33 +257,6 @@ 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>';
@@ -286,7 +267,7 @@ function renderComponents(components, total) {
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">';
sorted.forEach((c, idx) => {
components.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 : '—';
@@ -311,10 +292,7 @@ function renderComponents(components, total) {
settings.push('РУЧН');
}
// 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 += '<tr class="hover:bg-gray-50 cursor-pointer" onclick="openModal(' + idx + ')">';
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>';
@@ -352,6 +330,7 @@ function openModal(idx) {
document.getElementById('modal-manual-price').disabled = !hasManual;
// Reset price displays while loading
document.getElementById('modal-last-price').textContent = '...';
document.getElementById('modal-median-all').textContent = '...';
document.getElementById('modal-current-price').textContent = '...';
document.getElementById('modal-new-price').textContent = '...';
@@ -393,6 +372,18 @@ async function fetchPreview() {
if (resp.ok) {
const data = await resp.json();
// Update last price with date
if (data.last_price) {
let lastPriceText = '$' + parseFloat(data.last_price).toFixed(2);
if (data.last_price_date) {
const date = new Date(data.last_price_date);
lastPriceText += ' (' + date.toLocaleDateString('ru-RU') + ')';
}
document.getElementById('modal-last-price').textContent = lastPriceText;
} else {
document.getElementById('modal-last-price').textContent = '—';
}
// Update median all time
document.getElementById('modal-median-all').textContent =
data.median_all_time ? '$' + parseFloat(data.median_all_time).toFixed(2) : '—';
@@ -410,7 +401,9 @@ async function fetchPreview() {
}
} catch(e) {
console.error('Preview fetch error:', e);
document.getElementById('modal-last-price').textContent = '—';
document.getElementById('modal-median-all').textContent = '—';
document.getElementById('modal-current-price').textContent = '—';
document.getElementById('modal-new-price').textContent = '—';
}
}
@@ -584,13 +577,15 @@ document.getElementById('price-modal').addEventListener('click', function(e) {
function changeSort() {
sortField = document.getElementById('sort-field').value;
renderComponents(componentsCache, componentsCache.length);
currentPage = 1;
loadData();
}
function toggleSortDir() {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
document.getElementById('sort-dir-btn').textContent = sortDir === 'asc' ? '↑' : '↓';
renderComponents(componentsCache, componentsCache.length);
currentPage = 1;
loadData();
}
document.addEventListener('DOMContentLoaded', () => {