Исправления расчёта цен и добавление функционала своей цены

- Исправлен расчёт цен: теперь учитывается метод (медиана/среднее) и период для каждого компонента
- Добавлены функции calculateMedian и calculateAverage
- Исправлен PreviewPrice для корректного предпросмотра с учётом настроек
- Сортировка по умолчанию изменена на популярность (desc)
- Добавлен раздел "Своя цена" в конфигуратор:
  - Ввод целевой цены с пропорциональным пересчётом всех позиций
  - Отображение скидки в процентах
  - Таблица скорректированных цен
  - Экспорт CSV со скидкой

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-01-27 11:53:39 +03:00
parent 7ded78f2c3
commit db37040399
3 changed files with 350 additions and 138 deletions

View File

@@ -69,6 +69,66 @@
<button onclick="exportCSV()" class="px-3 py-1 bg-gray-200 text-gray-700 rounded text-sm hover:bg-gray-300">Экспорт CSV</button>
</div>
</div>
<!-- Custom price section -->
<div id="custom-price-section" class="bg-white rounded-lg shadow p-4">
<h3 class="font-semibold mb-3">Своя цена</h3>
<div class="flex items-center gap-4 mb-4">
<div class="flex-1">
<label class="block text-sm text-gray-600 mb-1">Введите целевую цену</label>
<div class="flex items-center gap-2">
<span class="text-gray-500">$</span>
<input type="number" id="custom-price-input" step="0.01" min="0"
placeholder="0.00"
class="flex-1 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
oninput="calculateCustomPrice()">
<button onclick="clearCustomPrice()" class="px-3 py-2 text-gray-500 hover:text-gray-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<div id="discount-info" class="text-right hidden">
<div class="text-sm text-gray-600">Скидка</div>
<div class="text-2xl font-bold text-green-600" id="discount-percent">0%</div>
</div>
</div>
<!-- Adjusted prices table -->
<div id="adjusted-prices" class="hidden">
<div class="border-t pt-3">
<h4 class="text-sm font-medium text-gray-700 mb-2">Скорректированные цены</h4>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Компонент</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Кол-во</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Было</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Стало</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Итого</th>
</tr>
</thead>
<tbody id="adjusted-prices-body" class="divide-y"></tbody>
<tfoot class="bg-gray-50 font-medium">
<tr>
<td class="px-3 py-2" colspan="2">Итого</td>
<td class="px-3 py-2 text-right" id="adjusted-total-original">$0.00</td>
<td class="px-3 py-2 text-right text-green-600" id="adjusted-total-new">$0.00</td>
<td class="px-3 py-2 text-right text-green-600" id="adjusted-total-final">$0.00</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="mt-3 flex justify-end">
<button onclick="exportCSVWithCustomPrice()" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">
Экспорт CSV со скидкой
</button>
</div>
</div>
</div>
</div>
<!-- Autocomplete dropdown (shared) -->
@@ -643,6 +703,9 @@ function updateCartUI() {
const total = cart.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
document.getElementById('cart-total').textContent = '$' + total.toLocaleString('en-US', {minimumFractionDigits: 2});
// Recalculate custom price section if active
calculateCustomPrice();
if (cart.length === 0) {
document.getElementById('cart-items').innerHTML =
'<div class="text-gray-500 text-center py-2">Конфигурация пуста</div>';
@@ -752,6 +815,113 @@ async function exportCSV() {
}
}
// Custom price functionality
function calculateCustomPrice() {
const customPriceInput = document.getElementById('custom-price-input');
const customPrice = parseFloat(customPriceInput.value) || 0;
const originalTotal = cart.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
if (customPrice <= 0 || cart.length === 0 || originalTotal <= 0) {
document.getElementById('adjusted-prices').classList.add('hidden');
document.getElementById('discount-info').classList.add('hidden');
return;
}
// Calculate discount percentage
const discountPercent = ((originalTotal - customPrice) / originalTotal) * 100;
const coefficient = customPrice / originalTotal;
// Show discount info
document.getElementById('discount-info').classList.remove('hidden');
document.getElementById('discount-percent').textContent = discountPercent.toFixed(1) + '%';
// Update discount color based on value
const discountEl = document.getElementById('discount-percent');
if (discountPercent > 0) {
discountEl.className = 'text-2xl font-bold text-green-600';
} else if (discountPercent < 0) {
discountEl.className = 'text-2xl font-bold text-red-600';
} else {
discountEl.className = 'text-2xl font-bold text-gray-600';
}
// Build adjusted prices table
let html = '';
let totalOriginal = 0;
let totalNew = 0;
cart.forEach(item => {
const originalPrice = item.unit_price;
const newPrice = originalPrice * coefficient;
const itemOriginalTotal = originalPrice * item.quantity;
const itemNewTotal = newPrice * item.quantity;
totalOriginal += itemOriginalTotal;
totalNew += itemNewTotal;
html += `
<tr>
<td class="px-3 py-2 font-mono">${escapeHtml(item.lot_name)}</td>
<td class="px-3 py-2 text-right">${item.quantity}</td>
<td class="px-3 py-2 text-right text-gray-500">$${originalPrice.toFixed(2)}</td>
<td class="px-3 py-2 text-right text-green-600">$${newPrice.toFixed(2)}</td>
<td class="px-3 py-2 text-right">$${itemNewTotal.toFixed(2)}</td>
</tr>
`;
});
document.getElementById('adjusted-prices-body').innerHTML = html;
document.getElementById('adjusted-total-original').textContent = '$' + totalOriginal.toFixed(2);
document.getElementById('adjusted-total-new').textContent = '$' + totalNew.toFixed(2);
document.getElementById('adjusted-total-final').textContent = '$' + totalNew.toFixed(2);
document.getElementById('adjusted-prices').classList.remove('hidden');
}
function clearCustomPrice() {
document.getElementById('custom-price-input').value = '';
document.getElementById('adjusted-prices').classList.add('hidden');
document.getElementById('discount-info').classList.add('hidden');
}
async function exportCSVWithCustomPrice() {
if (cart.length === 0) return;
const customPrice = parseFloat(document.getElementById('custom-price-input').value) || 0;
const originalTotal = cart.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
if (customPrice <= 0 || originalTotal <= 0) {
showToast('Введите целевую цену', 'error');
return;
}
const coefficient = customPrice / originalTotal;
// Create adjusted cart
const adjustedCart = cart.map(item => ({
...item,
unit_price: parseFloat((item.unit_price * coefficient).toFixed(2))
}));
try {
const resp = await fetch('/api/export/csv', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({items: adjustedCart, name: configName})
});
const blob = await resp.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (configName || 'config') + '.csv';
a.click();
window.URL.revokeObjectURL(url);
} catch(e) {
showToast('Ошибка экспорта', 'error');
}
}
</script>
{{end}}