Add hide component feature, usage indicators, and Docker support
- Add is_hidden field to hide components from configurator - Add colored dot indicator showing component usage status: - Green: available in configurator - Cyan: used as source for meta-articles - Gray: hidden from configurator - Optimize price recalculation with caching and skip unchanged - Show current lot name during price recalculation - Add Dockerfile (Alpine-based multi-stage build) - Add docker-compose.yml and .dockerignore Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,37 +81,25 @@
|
||||
<input type="checkbox" id="modal-meta-enabled" class="mr-2" onchange="toggleMetaPrice()">
|
||||
<span class="text-sm font-medium text-gray-700">Мета артикул</span>
|
||||
</div>
|
||||
<div id="meta-price-fields" class="hidden grid grid-cols-2 gap-4 mt-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Показатели цен</label>
|
||||
<input type="text" id="modal-meta-prices" class="w-full px-3 py-2 border rounded" placeholder="Артикулы через запятую (например: CPU_AMD_9654, MB_INTEL_4.Sapphire_2S)">
|
||||
<p class="text-xs text-gray-500 mt-1">Артикулы, чьи цены будут использоваться в расчете. <br>Для автоматического подбора используйте * в конце названия (например: CPU_AMD_9654*)</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Метод расчета</label>
|
||||
<select id="modal-meta-method" class="w-full px-3 py-2 border rounded">
|
||||
<option value="median">Медиана</option>
|
||||
<option value="average">Среднее</option>
|
||||
<option value="weighted_median">Взвешенная медиана</option>
|
||||
</select>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-2">Период расчета</label>
|
||||
<select id="modal-meta-period" class="w-full px-3 py-2 border rounded">
|
||||
<option value="7">1 неделя</option>
|
||||
<option value="30">1 месяц</option>
|
||||
<option value="90" selected>1 квартал</option>
|
||||
<option value="365">1 год</option>
|
||||
<option value="0">Всё время</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="meta-price-fields" class="hidden mt-2">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Источники цен</label>
|
||||
<input type="text" id="modal-meta-prices" class="w-full px-3 py-2 border rounded" placeholder="Артикулы через запятую (например: CPU_AMD_9654, MB_INTEL_4.Sapphire_2S)">
|
||||
<p class="text-xs text-gray-500 mt-1">Артикулы, чьи цены будут использоваться в расчете. Для автоматического подбора используйте * в конце (например: CPU_AMD_9654*)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Метод расчёта</label>
|
||||
<select id="modal-method" class="w-full px-3 py-2 border rounded">
|
||||
<select id="modal-method" class="w-full px-3 py-2 border rounded" onchange="onMethodChange()">
|
||||
<option value="median">Медиана</option>
|
||||
<option value="average">Среднее</option>
|
||||
<option value="manual">Установить цену вручную</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="manual-price-field" class="hidden">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Ручная цена (USD)</label>
|
||||
<input type="number" id="modal-manual-price" step="0.01" class="w-full px-3 py-2 border rounded" placeholder="Цена USD">
|
||||
<p class="text-xs text-gray-500 mt-1">Ручная цена сохраняется при пересчёте</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Период расчёта</label>
|
||||
@@ -130,42 +118,9 @@
|
||||
<p class="text-xs text-gray-500 mt-1">Например: 30 для +30%, -10 для -10%</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4">
|
||||
<label class="flex items-center mb-2">
|
||||
<input type="checkbox" id="modal-meta-enabled" class="mr-2" onchange="toggleMetaPrice()">
|
||||
<span class="text-sm font-medium text-gray-700">Мета артикул</span>
|
||||
</label>
|
||||
<div id="meta-price-fields" class="hidden grid grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Показатели цен</label>
|
||||
<input type="text" id="modal-meta-prices" class="w-full px-3 py-2 border rounded" placeholder="Артикулы через запятую (например: CPU_AMD_9654, MB_INTEL_4.Sapphire_2S)">
|
||||
<p class="text-xs text-gray-500 mt-1">Артикулы, чьи цены будут использоваться в расчете. <br>Для автоматического подбора используйте * в конце названия (например: CPU_AMD_9654*)</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Метод расчета</label>
|
||||
<select id="modal-meta-method" class="w-full px-3 py-2 border rounded">
|
||||
<option value="median">Медиана</option>
|
||||
<option value="average">Среднее</option>
|
||||
<option value="weighted_median">Взвешенная медиана</option>
|
||||
</select>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1 mt-2">Период расчета</label>
|
||||
<select id="modal-meta-period" class="w-full px-3 py-2 border rounded">
|
||||
<option value="7">1 неделя</option>
|
||||
<option value="30">1 месяц</option>
|
||||
<option value="90" selected>1 квартал</option>
|
||||
<option value="365">1 год</option>
|
||||
<option value="0">Всё время</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="flex items-center mb-2">
|
||||
<input type="checkbox" id="modal-manual-enabled" class="mr-2" onchange="toggleManualPrice()">
|
||||
<span class="text-sm font-medium text-gray-700">Установить цену вручную</span>
|
||||
</label>
|
||||
<input type="number" id="modal-manual-price" step="0.01" class="w-full px-3 py-2 border rounded" placeholder="Цена USD" disabled>
|
||||
<p class="text-xs text-gray-500 mt-1">Ручная цена сохраняется при пересчёте</p>
|
||||
</div>
|
||||
<div class="flex items-center pt-2 border-t">
|
||||
<input type="checkbox" id="modal-hidden" class="mr-2">
|
||||
<span class="text-sm font-medium text-gray-700">Скрыть из конфигуратора</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-3 rounded space-y-2">
|
||||
@@ -360,33 +315,77 @@ function renderComponents(components, total) {
|
||||
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';
|
||||
const isHidden = c.is_hidden || quoteCount === 0;
|
||||
const usedInMeta = c.used_in_meta && c.used_in_meta.length > 0;
|
||||
|
||||
// Determine status indicator (colored dot)
|
||||
let dotColor, dotTitle;
|
||||
if (usedInMeta) {
|
||||
// Used as source for meta-articles - cyan
|
||||
dotColor = 'bg-cyan-500';
|
||||
dotTitle = 'Используется в мета: ' + c.used_in_meta.join(', ');
|
||||
} else if (!isHidden) {
|
||||
// Available in configurator - green
|
||||
dotColor = 'bg-green-500';
|
||||
dotTitle = 'Доступен в конфигураторе';
|
||||
} else {
|
||||
// Hidden and not used - gray
|
||||
dotColor = 'bg-gray-400';
|
||||
dotTitle = 'Скрыт из конфигуратора';
|
||||
}
|
||||
|
||||
// Build settings summary
|
||||
let settings = [];
|
||||
const method = c.price_method || 'median';
|
||||
settings.push(method === 'median' ? 'М' : 'С');
|
||||
const period = c.price_period_days !== undefined && c.price_period_days !== null ? c.price_period_days : 90;
|
||||
if (period === 7) settings.push('1н');
|
||||
else if (period === 30) settings.push('1м');
|
||||
else if (period === 90) settings.push('3м');
|
||||
else if (period === 365) settings.push('1г');
|
||||
else if (period === 0) settings.push('все');
|
||||
else settings.push(period + 'д');
|
||||
if (c.price_coefficient && c.price_coefficient !== 0) {
|
||||
settings.push((c.price_coefficient > 0 ? '+' : '') + c.price_coefficient + '%');
|
||||
}
|
||||
if (c.manual_price && c.manual_price > 0) {
|
||||
settings.push('РУЧН');
|
||||
let settingsHtml = '';
|
||||
|
||||
if (isHidden) {
|
||||
settingsHtml = '<span class="text-red-600 font-medium">СКРЫТ</span>';
|
||||
} else {
|
||||
let settings = [];
|
||||
const method = c.price_method || 'median';
|
||||
const hasManualPrice = c.manual_price && c.manual_price > 0;
|
||||
const hasMeta = c.meta_prices && c.meta_prices.trim() !== '';
|
||||
|
||||
// Method indicator
|
||||
if (hasManualPrice) {
|
||||
settings.push('<span class="text-orange-600 font-medium">РУЧН</span>');
|
||||
} else if (method === 'average') {
|
||||
settings.push('Сред');
|
||||
} else {
|
||||
settings.push('Мед');
|
||||
}
|
||||
|
||||
// Period (only if not manual price)
|
||||
if (!hasManualPrice) {
|
||||
const period = c.price_period_days !== undefined && c.price_period_days !== null ? c.price_period_days : 90;
|
||||
if (period === 7) settings.push('1н');
|
||||
else if (period === 30) settings.push('1м');
|
||||
else if (period === 90) settings.push('3м');
|
||||
else if (period === 365) settings.push('1г');
|
||||
else if (period === 0) settings.push('все');
|
||||
else settings.push(period + 'д');
|
||||
}
|
||||
|
||||
// Coefficient
|
||||
if (c.price_coefficient && c.price_coefficient !== 0) {
|
||||
settings.push((c.price_coefficient > 0 ? '+' : '') + c.price_coefficient + '%');
|
||||
}
|
||||
|
||||
// Meta article indicator
|
||||
if (hasMeta) {
|
||||
settings.push('<span class="text-purple-600 font-medium">МЕТА</span>');
|
||||
}
|
||||
|
||||
settingsHtml = settings.join(' | ');
|
||||
}
|
||||
|
||||
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 font-mono"><span class="inline-flex items-center gap-2"><span class="w-2.5 h-2.5 rounded-full ' + dotColor + ' flex-shrink-0" title="' + escapeHtml(dotTitle) + '"></span>' + escapeHtml(c.lot_name) + '</span></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>';
|
||||
html += '<td class="px-3 py-2 text-sm"><span class="text-xs bg-gray-100 px-2 py-1 rounded">' + settingsHtml + '</span></td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
@@ -407,14 +406,41 @@ function openModal(idx) {
|
||||
if (!c) return;
|
||||
|
||||
document.getElementById('modal-lot-name').value = c.lot_name;
|
||||
document.getElementById('modal-method').value = c.price_method || 'median';
|
||||
document.getElementById('modal-period').value = String(c.price_period_days !== undefined && c.price_period_days !== null ? c.price_period_days : 90);
|
||||
document.getElementById('modal-coefficient').value = c.price_coefficient || 0;
|
||||
|
||||
const hasManual = c.manual_price && c.manual_price > 0;
|
||||
document.getElementById('modal-manual-enabled').checked = hasManual;
|
||||
document.getElementById('modal-manual-price').value = hasManual ? c.manual_price : '';
|
||||
document.getElementById('modal-manual-price').disabled = !hasManual;
|
||||
if (hasManual) {
|
||||
document.getElementById('modal-method').value = 'manual';
|
||||
document.getElementById('modal-manual-price').value = c.manual_price;
|
||||
document.getElementById('manual-price-field').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('modal-method').value = c.price_method || 'median';
|
||||
document.getElementById('modal-manual-price').value = '';
|
||||
document.getElementById('manual-price-field').classList.add('hidden');
|
||||
}
|
||||
document.getElementById('modal-period').value = String(c.price_period_days !== undefined && c.price_period_days !== null ? c.price_period_days : 90);
|
||||
|
||||
// Load meta prices settings
|
||||
const hasMeta = c.meta_prices && c.meta_prices.trim() !== '';
|
||||
document.getElementById('modal-meta-enabled').checked = hasMeta;
|
||||
document.getElementById('modal-meta-prices').value = c.meta_prices || '';
|
||||
if (hasMeta) {
|
||||
document.getElementById('meta-price-fields').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('meta-price-fields').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Load hidden flag
|
||||
const quoteCount = c.quote_count || 0;
|
||||
const hiddenCheckbox = document.getElementById('modal-hidden');
|
||||
if (quoteCount === 0) {
|
||||
// Если нет котировок - чекбокс установлен и заблокирован
|
||||
hiddenCheckbox.checked = true;
|
||||
hiddenCheckbox.disabled = true;
|
||||
} else {
|
||||
hiddenCheckbox.checked = c.is_hidden || false;
|
||||
hiddenCheckbox.disabled = false;
|
||||
}
|
||||
|
||||
// Reset price displays while loading
|
||||
document.getElementById('modal-last-price').textContent = '...';
|
||||
@@ -430,6 +456,20 @@ function openModal(idx) {
|
||||
fetchPreview();
|
||||
}
|
||||
|
||||
function onMethodChange() {
|
||||
const method = document.getElementById('modal-method').value;
|
||||
const manualField = document.getElementById('manual-price-field');
|
||||
if (method === 'manual') {
|
||||
manualField.classList.remove('hidden');
|
||||
// При выборе "Установить цену вручную" снимаем галочку "Мета артикул"
|
||||
document.getElementById('modal-meta-enabled').checked = false;
|
||||
document.getElementById('meta-price-fields').classList.add('hidden');
|
||||
} else {
|
||||
manualField.classList.add('hidden');
|
||||
}
|
||||
fetchPreview();
|
||||
}
|
||||
|
||||
async function fetchPreview() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
@@ -445,8 +485,8 @@ async function fetchPreview() {
|
||||
|
||||
if (metaEnabled) {
|
||||
metaPrices = document.getElementById('modal-meta-prices').value.trim();
|
||||
metaMethod = document.getElementById('modal-meta-method').value;
|
||||
metaPeriod = parseInt(document.getElementById('modal-meta-period').value) || 0;
|
||||
metaMethod = method;
|
||||
metaPeriod = periodDays;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -520,32 +560,18 @@ function toggleMetaPrice() {
|
||||
fields.classList.toggle('hidden', !enabled);
|
||||
|
||||
if (enabled) {
|
||||
// When enabling meta price, disable manual price
|
||||
document.getElementById('modal-manual-enabled').checked = false;
|
||||
document.getElementById('modal-manual-price').disabled = true;
|
||||
document.getElementById('modal-manual-price').value = '';
|
||||
// When enabling meta price, reset method to median if it was manual
|
||||
const method = document.getElementById('modal-method').value;
|
||||
if (method === 'manual') {
|
||||
document.getElementById('modal-method').value = 'median';
|
||||
document.getElementById('manual-price-field').classList.add('hidden');
|
||||
document.getElementById('modal-manual-price').value = '';
|
||||
}
|
||||
// Auto-fill with wildcard pattern
|
||||
const lotName = document.getElementById('modal-lot-name').value;
|
||||
if (lotName) {
|
||||
autoFillMetaPrices(lotName);
|
||||
}
|
||||
} else {
|
||||
// When disabling meta price, reset to default values
|
||||
// Don't change the main settings - they should stay as they were
|
||||
}
|
||||
fetchPreview();
|
||||
}
|
||||
|
||||
function toggleManualPrice() {
|
||||
const enabled = document.getElementById('modal-manual-enabled').checked;
|
||||
document.getElementById('modal-manual-price').disabled = !enabled;
|
||||
if (!enabled) {
|
||||
document.getElementById('modal-manual-price').value = '';
|
||||
}
|
||||
// When enabling manual price, disable meta price
|
||||
if (enabled) {
|
||||
document.getElementById('modal-meta-enabled').checked = false;
|
||||
document.getElementById('meta-price-fields').classList.add('hidden');
|
||||
}
|
||||
fetchPreview();
|
||||
}
|
||||
@@ -569,9 +595,12 @@ async function savePrice() {
|
||||
const periodDaysStr = document.getElementById('modal-period').value;
|
||||
const periodDays = periodDaysStr !== '' ? parseInt(periodDaysStr) : 90;
|
||||
const coefficient = parseFloat(document.getElementById('modal-coefficient').value) || 0;
|
||||
const manualEnabled = document.getElementById('modal-manual-enabled').checked;
|
||||
const manualEnabled = method === 'manual';
|
||||
const manualPrice = manualEnabled ? parseFloat(document.getElementById('modal-manual-price').value) : null;
|
||||
const metaEnabled = document.getElementById('modal-meta-enabled').checked;
|
||||
const hiddenCheckbox = document.getElementById('modal-hidden');
|
||||
// Если чекбокс заблокирован (нет котировок), всегда true
|
||||
const isHidden = hiddenCheckbox.disabled ? true : hiddenCheckbox.checked;
|
||||
|
||||
let metaPrices = '';
|
||||
let metaMethod = '';
|
||||
@@ -579,20 +608,21 @@ async function savePrice() {
|
||||
|
||||
if (metaEnabled) {
|
||||
metaPrices = document.getElementById('modal-meta-prices').value.trim();
|
||||
metaMethod = document.getElementById('modal-meta-method').value;
|
||||
metaPeriod = parseInt(document.getElementById('modal-meta-period').value) || 0;
|
||||
metaMethod = manualEnabled ? 'median' : method;
|
||||
metaPeriod = periodDays;
|
||||
}
|
||||
|
||||
const body = {
|
||||
lot_name: lotName,
|
||||
method: method,
|
||||
method: manualEnabled ? 'median' : method,
|
||||
period_days: periodDays,
|
||||
coefficient: coefficient,
|
||||
clear_manual: !manualEnabled,
|
||||
meta_enabled: metaEnabled,
|
||||
meta_prices: metaPrices,
|
||||
meta_method: metaMethod,
|
||||
meta_period: metaPeriod
|
||||
meta_period: metaPeriod,
|
||||
is_hidden: isHidden
|
||||
};
|
||||
|
||||
if (manualEnabled && manualPrice > 0) {
|
||||
@@ -716,10 +746,10 @@ function recalculateAll() {
|
||||
progressText.textContent = 'Пересчёт завершён!';
|
||||
progressBar.className = 'bg-green-600 h-4 rounded-full';
|
||||
} else {
|
||||
progressText.textContent = 'Обработка компонентов...';
|
||||
progressText.textContent = data.lot_name ? 'Обработка: ' + data.lot_name : 'Обработка компонентов...';
|
||||
}
|
||||
|
||||
progressStats.textContent = 'Обновлено: ' + (data.updated || 0) + ' | Ручные: ' + (data.manual || 0) + ' | Нет данных: ' + (data.skipped || 0) + ' | Ошибок: ' + (data.errors || 0);
|
||||
progressStats.textContent = 'Обновлено: ' + (data.updated || 0) + ' | Без изменений: ' + (data.unchanged || 0) + ' | Ручные: ' + (data.manual || 0) + ' | Нет данных: ' + (data.skipped || 0) + ' | Ошибок: ' + (data.errors || 0);
|
||||
} catch(e) {
|
||||
console.log('Parse error:', e, line);
|
||||
}
|
||||
@@ -815,9 +845,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadTab('alerts');
|
||||
|
||||
// Add event listeners for preview updates
|
||||
document.getElementById('modal-method').addEventListener('change', fetchPreview);
|
||||
document.getElementById('modal-period').addEventListener('change', fetchPreview);
|
||||
document.getElementById('modal-coefficient').addEventListener('input', debounceFetchPreview);
|
||||
document.getElementById('modal-manual-price').addEventListener('input', debounceFetchPreview);
|
||||
document.getElementById('modal-meta-prices').addEventListener('input', debounceFetchPreview);
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user