Улучшения управления ценами и конфигурациями
- Добавлено отображение последней полученной цены в окне настройки цены - Добавлен функционал переименования конфигураций (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:
@@ -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', () => {
|
||||
|
||||
@@ -39,6 +39,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for renaming configuration -->
|
||||
<div id="rename-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Переименовать конфигурацию</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Новое название</label>
|
||||
<input type="text" id="rename-input" placeholder="Введите новое название"
|
||||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<input type="hidden" id="rename-uuid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button onclick="closeRenameModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
||||
Отмена
|
||||
</button>
|
||||
<button onclick="renameConfig()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadConfigs() {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -95,7 +120,8 @@ function renderConfigs(configs) {
|
||||
html += '<td class="px-4 py-3 text-sm font-medium"><a href="/configurator?uuid=' + c.uuid + '" class="text-blue-600 hover:text-blue-800 hover:underline">' + escapeHtml(c.name) + '</a></td>';
|
||||
html += '<td class="px-4 py-3 text-sm text-gray-500">' + date + '</td>';
|
||||
html += '<td class="px-4 py-3 text-sm text-right">' + total + '</td>';
|
||||
html += '<td class="px-4 py-3 text-sm text-right">';
|
||||
html += '<td class="px-4 py-3 text-sm text-right space-x-2">';
|
||||
html += '<button onclick="openRenameModal(\'' + c.uuid + '\', \'' + escapeHtml(c.name).replace(/'/g, "\\'") + '\')" class="text-blue-600 hover:text-blue-800">Переименовать</button>';
|
||||
html += '<button onclick="deleteConfig(\'' + c.uuid + '\')" class="text-red-600 hover:text-red-800">Удалить</button>';
|
||||
html += '</td></tr>';
|
||||
});
|
||||
@@ -120,6 +146,63 @@ async function deleteConfig(uuid) {
|
||||
loadConfigs();
|
||||
}
|
||||
|
||||
function openRenameModal(uuid, currentName) {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
document.getElementById('rename-uuid').value = uuid;
|
||||
document.getElementById('rename-input').value = currentName;
|
||||
document.getElementById('rename-modal').classList.remove('hidden');
|
||||
document.getElementById('rename-modal').classList.add('flex');
|
||||
document.getElementById('rename-input').focus();
|
||||
document.getElementById('rename-input').select();
|
||||
}
|
||||
|
||||
function closeRenameModal() {
|
||||
document.getElementById('rename-modal').classList.add('hidden');
|
||||
document.getElementById('rename-modal').classList.remove('flex');
|
||||
}
|
||||
|
||||
async function renameConfig() {
|
||||
const token = localStorage.getItem('token');
|
||||
const uuid = document.getElementById('rename-uuid').value;
|
||||
const name = document.getElementById('rename-input').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('Введите название');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/configs/' + uuid + '/rename', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name: name })
|
||||
});
|
||||
|
||||
if (resp.status === 401) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
alert('Ошибка: ' + (err.error || 'Не удалось переименовать'));
|
||||
return;
|
||||
}
|
||||
|
||||
closeRenameModal();
|
||||
loadConfigs();
|
||||
} catch(e) {
|
||||
alert('Ошибка переименования');
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
@@ -185,10 +268,24 @@ document.getElementById('create-modal').addEventListener('click', function(e) {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('rename-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeRenameModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeCreateModal();
|
||||
closeRenameModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Submit rename on Enter key
|
||||
document.getElementById('rename-input').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
renameConfig();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user