WIP: save current pricing and pricelist changes
This commit is contained in:
@@ -15,9 +15,15 @@
|
||||
</h1>
|
||||
</div>
|
||||
<div id="save-buttons" class="hidden flex items-center space-x-2">
|
||||
<button onclick="refreshPrices()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||
<button id="refresh-prices-btn" onclick="refreshPrices()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||
Обновить цены
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="openPriceSettingsModal()"
|
||||
class="h-10 px-3 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 border border-gray-300 inline-flex items-center justify-center"
|
||||
title="Настройки цен">
|
||||
Цены
|
||||
</button>
|
||||
<button onclick="saveConfig()" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">
|
||||
Сохранить
|
||||
</button>
|
||||
@@ -34,13 +40,9 @@
|
||||
class="w-20 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
|
||||
onchange="updateServerCount()">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Прайслист</label>
|
||||
<select id="pricelist-select"
|
||||
class="w-56 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
|
||||
onchange="updatePricelistSelection()">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
<div class="text-sm text-gray-600">
|
||||
<div class="font-medium text-gray-700 mb-1">Прайслисты цен</div>
|
||||
<div id="pricelist-settings-summary">Estimate: авто, Склад: авто, Конкуренты: авто</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<span id="server-count-info">Всего: <span id="total-server-count">1</span> сервер(а)</span>
|
||||
@@ -86,20 +88,37 @@
|
||||
</div>
|
||||
|
||||
<!-- Cart summary -->
|
||||
<div id="cart-summary" class="bg-white rounded-lg shadow p-4">
|
||||
<h3 class="font-semibold mb-3">Итого конфигурация</h3>
|
||||
<div id="cart-items" class="space-y-2 mb-4"></div>
|
||||
<div class="border-t pt-3 flex justify-between items-center">
|
||||
<div class="text-lg font-bold">
|
||||
Итого: <span id="cart-total">$0.00</span>
|
||||
<div id="cart-summary" class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<button type="button"
|
||||
onclick="toggleCartSummarySection()"
|
||||
class="w-full px-4 py-3 flex items-center justify-between text-blue-900 bg-gradient-to-r from-blue-100 to-blue-50 hover:from-blue-200 hover:to-blue-100 border-b border-blue-200">
|
||||
<span class="font-semibold">Итого конфигурация</span>
|
||||
<svg id="cart-summary-toggle-icon" class="w-5 h-5 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="cart-summary-content" class="p-4">
|
||||
<div id="cart-items" class="space-y-2 mb-4"></div>
|
||||
<div class="border-t pt-3 flex justify-between items-center">
|
||||
<div class="text-lg font-bold">
|
||||
Итого: <span id="cart-total">$0.00</span>
|
||||
</div>
|
||||
<button onclick="exportCSV()" class="px-3 py-1 bg-gray-200 text-gray-700 rounded text-sm hover:bg-gray-300">Экспорт CSV</button>
|
||||
</div>
|
||||
<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 id="custom-price-section" class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<button type="button"
|
||||
onclick="toggleCustomPriceSection()"
|
||||
class="w-full px-4 py-3 flex items-center justify-between text-blue-900 bg-gradient-to-r from-blue-100 to-blue-50 hover:from-blue-200 hover:to-blue-100 border-b border-blue-200">
|
||||
<span class="font-semibold">Своя цена</span>
|
||||
<svg id="custom-price-toggle-icon" class="w-5 h-5 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="custom-price-content" class="p-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm text-gray-600 mb-1">Введите целевую цену</label>
|
||||
@@ -155,6 +174,83 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sale price section -->
|
||||
<div id="sale-price-section" class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<button type="button"
|
||||
onclick="toggleSalePriceSection()"
|
||||
class="w-full px-4 py-3 flex items-center justify-between text-blue-900 bg-gradient-to-r from-blue-100 to-blue-50 hover:from-blue-200 hover:to-blue-100 border-b border-blue-200">
|
||||
<span class="font-semibold">Цена продажи</span>
|
||||
<svg id="sale-price-toggle-icon" class="w-5 h-5 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="sale-price-content" class="p-4">
|
||||
<div id="sale-prices" class="border-t pt-3">
|
||||
<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">Est. Price</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="sale-prices-body" class="divide-y"></tbody>
|
||||
<tfoot class="bg-gray-50 font-medium">
|
||||
<tr>
|
||||
<td class="px-3 py-2">Итого</td>
|
||||
<td class="px-3 py-2 text-right">—</td>
|
||||
<td class="px-3 py-2 text-right" id="sale-total-est">$0.00</td>
|
||||
<td class="px-3 py-2 text-right" id="sale-total-warehouse">$0.00</td>
|
||||
<td class="px-3 py-2 text-right" id="sale-total-competitor">$0.00</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price settings modal -->
|
||||
<div id="price-settings-modal" class="hidden fixed inset-0 z-50">
|
||||
<div class="absolute inset-0 bg-black/40" onclick="closePriceSettingsModal()"></div>
|
||||
<div class="relative max-w-xl mx-auto mt-24 bg-white rounded-lg shadow-xl border">
|
||||
<div class="px-5 py-4 border-b flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Настройки цен</h3>
|
||||
<button type="button" onclick="closePriceSettingsModal()" class="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 class="px-5 py-4 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Estimate</label>
|
||||
<select id="settings-pricelist-estimate" class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Склад</label>
|
||||
<select id="settings-pricelist-warehouse" class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Конкуренты</label>
|
||||
<select id="settings-pricelist-competitor" class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"></select>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700">
|
||||
<input id="settings-disable-price-refresh" type="checkbox" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<span>Не обновлять цены</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="px-5 py-4 border-t flex justify-end gap-2">
|
||||
<button type="button" onclick="closePriceSettingsModal()" class="px-4 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200">Отмена</button>
|
||||
<button type="button" onclick="applyPriceSettings()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Применить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -232,7 +328,18 @@ let cart = [];
|
||||
let categoryOrderMap = {}; // Category code -> display_order mapping
|
||||
let autoSaveTimeout = null; // Timeout for debounced autosave
|
||||
let serverCount = 1; // Server count for the configuration
|
||||
let selectedPricelistId = null; // Selected pricelist (server ID)
|
||||
let selectedPricelistIds = {
|
||||
estimate: null,
|
||||
warehouse: null,
|
||||
competitor: null
|
||||
};
|
||||
let disablePriceRefresh = false;
|
||||
let activePricelistsBySource = {
|
||||
estimate: [],
|
||||
warehouse: [],
|
||||
competitor: []
|
||||
};
|
||||
let priceLevelsRequestSeq = 0;
|
||||
|
||||
// Autocomplete state
|
||||
let autocompleteInput = null;
|
||||
@@ -241,6 +348,101 @@ let autocompleteMode = null; // 'single', 'multi', 'section'
|
||||
let autocompleteIndex = -1;
|
||||
let autocompleteFiltered = [];
|
||||
|
||||
function getDisplayPrice(item) {
|
||||
if (typeof item.unit_price === 'number' && item.unit_price > 0) {
|
||||
return item.unit_price;
|
||||
}
|
||||
if (typeof item.estimate_price === 'number' && item.estimate_price > 0) {
|
||||
return item.estimate_price;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function formatNumberRu(value) {
|
||||
const rounded = Math.round(value);
|
||||
return rounded
|
||||
.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: 0 })
|
||||
.replace(/[\u202f\u00a0 ]/g, '\u00A0');
|
||||
}
|
||||
|
||||
function formatMoney(value) {
|
||||
return '$\u00A0' + formatNumberRu(value);
|
||||
}
|
||||
|
||||
function formatPriceOrNA(value) {
|
||||
if (typeof value !== 'number' || value <= 0) {
|
||||
return 'N/A';
|
||||
}
|
||||
return formatMoney(value);
|
||||
}
|
||||
|
||||
function formatDelta(abs, pct) {
|
||||
if (typeof abs !== 'number') {
|
||||
return 'N/A';
|
||||
}
|
||||
const sign = abs > 0 ? '+' : abs < 0 ? '-' : '';
|
||||
const absValue = Math.abs(abs);
|
||||
if (typeof pct !== 'number') {
|
||||
return sign + formatMoney(absValue);
|
||||
}
|
||||
const pctSign = pct > 0 ? '+' : pct < 0 ? '-' : '';
|
||||
return sign + formatMoney(absValue) + ' (' + pctSign + Math.round(Math.abs(pct)) + '%)';
|
||||
}
|
||||
|
||||
async function refreshPriceLevels() {
|
||||
if (!configUUID || cart.length === 0 || disablePriceRefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seq = ++priceLevelsRequestSeq;
|
||||
try {
|
||||
const payload = {
|
||||
items: cart.map(item => ({
|
||||
lot_name: item.lot_name,
|
||||
quantity: item.quantity
|
||||
})),
|
||||
pricelist_ids: Object.fromEntries(
|
||||
Object.entries(selectedPricelistIds)
|
||||
.filter(([, id]) => typeof id === 'number' && id > 0)
|
||||
)
|
||||
};
|
||||
const resp = await fetch('/api/quote/price-levels', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
if (seq !== priceLevelsRequestSeq) {
|
||||
return;
|
||||
}
|
||||
const byLot = new Map((data.items || []).map(i => [i.lot_name, i]));
|
||||
cart = cart.map(item => {
|
||||
const levels = byLot.get(item.lot_name);
|
||||
if (!levels) return item;
|
||||
const next = { ...item, ...levels };
|
||||
if (typeof levels.estimate_price === 'number' && levels.estimate_price > 0) {
|
||||
next.unit_price = levels.estimate_price;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
if (data.resolved_pricelist_ids) {
|
||||
['estimate', 'warehouse', 'competitor'].forEach(source => {
|
||||
if (!selectedPricelistIds[source] && data.resolved_pricelist_ids[source]) {
|
||||
selectedPricelistIds[source] = data.resolved_pricelist_ids[source];
|
||||
}
|
||||
});
|
||||
syncPriceSettingsControls();
|
||||
renderPricelistSettingsSummary();
|
||||
persistLocalPriceSettings();
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Failed to refresh price levels', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Load categories from API and update tab configuration
|
||||
async function loadCategoriesFromAPI() {
|
||||
try {
|
||||
@@ -305,13 +507,16 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
serverCount = config.server_count || 1;
|
||||
document.getElementById('server-count').value = serverCount;
|
||||
document.getElementById('total-server-count').textContent = serverCount;
|
||||
selectedPricelistId = config.pricelist_id || null;
|
||||
selectedPricelistIds.estimate = config.pricelist_id || null;
|
||||
|
||||
if (config.items && config.items.length > 0) {
|
||||
cart = config.items.map(item => ({
|
||||
lot_name: item.lot_name,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unit_price,
|
||||
estimate_price: item.unit_price,
|
||||
warehouse_price: null,
|
||||
competitor_price: null,
|
||||
description: item.description || '',
|
||||
category: item.category || getCategoryFromLotName(item.lot_name)
|
||||
}));
|
||||
@@ -332,8 +537,13 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
return;
|
||||
}
|
||||
|
||||
restoreLocalPriceSettings();
|
||||
await loadActivePricelists();
|
||||
syncPriceSettingsControls();
|
||||
renderPricelistSettingsSummary();
|
||||
updateRefreshPricesButtonState();
|
||||
await loadAllComponents();
|
||||
await refreshPriceLevels();
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
|
||||
@@ -373,41 +583,148 @@ function updateServerCount() {
|
||||
}
|
||||
|
||||
async function loadActivePricelists() {
|
||||
const select = document.getElementById('pricelist-select');
|
||||
const sources = ['estimate', 'warehouse', 'competitor'];
|
||||
await Promise.all(sources.map(async source => {
|
||||
try {
|
||||
const resp = await fetch(`/api/pricelists?active_only=true&source=${source}&per_page=200`);
|
||||
const data = await resp.json();
|
||||
activePricelistsBySource[source] = data.pricelists || [];
|
||||
const existing = selectedPricelistIds[source];
|
||||
if (existing && activePricelistsBySource[source].some(pl => Number(pl.id) === Number(existing))) {
|
||||
return;
|
||||
}
|
||||
selectedPricelistIds[source] = activePricelistsBySource[source].length > 0
|
||||
? Number(activePricelistsBySource[source][0].id)
|
||||
: null;
|
||||
} catch (e) {
|
||||
activePricelistsBySource[source] = [];
|
||||
selectedPricelistIds[source] = null;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function renderPricelistSelectOptions(selectId, source) {
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select) return;
|
||||
const pricelists = activePricelistsBySource[source] || [];
|
||||
if (pricelists.length === 0) {
|
||||
select.innerHTML = '<option value="">Нет активных прайслистов</option>';
|
||||
select.value = '';
|
||||
return;
|
||||
}
|
||||
select.innerHTML = `<option value="">Авто (последний активный)</option>` + pricelists.map(pl => {
|
||||
return `<option value="${pl.id}">${escapeHtml(pl.version)}</option>`;
|
||||
}).join('');
|
||||
const current = selectedPricelistIds[source];
|
||||
select.value = current ? String(current) : '';
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/pricelists?active_only=true&per_page=200');
|
||||
const data = await resp.json();
|
||||
const pricelists = data.pricelists || [];
|
||||
|
||||
if (pricelists.length === 0) {
|
||||
select.innerHTML = '<option value="">Нет активных прайслистов</option>';
|
||||
selectedPricelistId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
select.innerHTML = pricelists.map(pl => {
|
||||
return `<option value="${pl.id}">${pl.version}</option>`;
|
||||
}).join('');
|
||||
|
||||
const existing = selectedPricelistId && pricelists.some(pl => Number(pl.id) === Number(selectedPricelistId));
|
||||
if (existing) {
|
||||
select.value = String(selectedPricelistId);
|
||||
} else {
|
||||
selectedPricelistId = Number(pricelists[0].id);
|
||||
select.value = String(selectedPricelistId);
|
||||
}
|
||||
} catch (e) {
|
||||
select.innerHTML = '<option value="">Ошибка загрузки</option>';
|
||||
function syncPriceSettingsControls() {
|
||||
renderPricelistSelectOptions('settings-pricelist-estimate', 'estimate');
|
||||
renderPricelistSelectOptions('settings-pricelist-warehouse', 'warehouse');
|
||||
renderPricelistSelectOptions('settings-pricelist-competitor', 'competitor');
|
||||
const disableCheckbox = document.getElementById('settings-disable-price-refresh');
|
||||
if (disableCheckbox) {
|
||||
disableCheckbox.checked = disablePriceRefresh;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePricelistSelection() {
|
||||
const select = document.getElementById('pricelist-select');
|
||||
const next = parseInt(select.value);
|
||||
selectedPricelistId = Number.isFinite(next) && next > 0 ? next : null;
|
||||
triggerAutoSave();
|
||||
function getPricelistVersionById(source, id) {
|
||||
const pricelists = activePricelistsBySource[source] || [];
|
||||
const found = pricelists.find(pl => Number(pl.id) === Number(id));
|
||||
return found ? found.version : null;
|
||||
}
|
||||
|
||||
function renderPricelistSettingsSummary() {
|
||||
const summary = document.getElementById('pricelist-settings-summary');
|
||||
if (!summary) return;
|
||||
const estimate = selectedPricelistIds.estimate ? getPricelistVersionById('estimate', selectedPricelistIds.estimate) || `ID ${selectedPricelistIds.estimate}` : 'авто';
|
||||
const warehouse = selectedPricelistIds.warehouse ? getPricelistVersionById('warehouse', selectedPricelistIds.warehouse) || `ID ${selectedPricelistIds.warehouse}` : 'авто';
|
||||
const competitor = selectedPricelistIds.competitor ? getPricelistVersionById('competitor', selectedPricelistIds.competitor) || `ID ${selectedPricelistIds.competitor}` : 'авто';
|
||||
const refreshState = disablePriceRefresh ? ' | Обновление цен: выкл' : '';
|
||||
summary.textContent = `Estimate: ${estimate}, Склад: ${warehouse}, Конкуренты: ${competitor}${refreshState}`;
|
||||
}
|
||||
|
||||
function updateRefreshPricesButtonState() {
|
||||
const refreshBtn = document.getElementById('refresh-prices-btn');
|
||||
if (!refreshBtn) return;
|
||||
if (disablePriceRefresh) {
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.className = 'px-4 py-2 bg-gray-300 text-gray-500 rounded cursor-not-allowed';
|
||||
refreshBtn.title = 'Обновление цен отключено в настройках';
|
||||
} else {
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.className = 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700';
|
||||
refreshBtn.title = '';
|
||||
}
|
||||
}
|
||||
|
||||
function getPriceSettingsStorageKey() {
|
||||
return `qf_price_settings_${configUUID || 'default'}`;
|
||||
}
|
||||
|
||||
function persistLocalPriceSettings() {
|
||||
try {
|
||||
localStorage.setItem(getPriceSettingsStorageKey(), JSON.stringify({
|
||||
pricelist_ids: selectedPricelistIds,
|
||||
disable_price_refresh: disablePriceRefresh
|
||||
}));
|
||||
} catch (e) {
|
||||
// ignore localStorage failures
|
||||
}
|
||||
}
|
||||
|
||||
function restoreLocalPriceSettings() {
|
||||
try {
|
||||
const raw = localStorage.getItem(getPriceSettingsStorageKey());
|
||||
if (!raw) return;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && parsed.pricelist_ids) {
|
||||
['estimate', 'warehouse', 'competitor'].forEach(source => {
|
||||
const next = parseInt(parsed.pricelist_ids[source]);
|
||||
if (Number.isFinite(next) && next > 0) {
|
||||
selectedPricelistIds[source] = next;
|
||||
}
|
||||
});
|
||||
}
|
||||
disablePriceRefresh = Boolean(parsed?.disable_price_refresh);
|
||||
} catch (e) {
|
||||
// ignore invalid localStorage payload
|
||||
}
|
||||
}
|
||||
|
||||
async function openPriceSettingsModal() {
|
||||
await loadActivePricelists();
|
||||
syncPriceSettingsControls();
|
||||
renderPricelistSettingsSummary();
|
||||
document.getElementById('price-settings-modal')?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closePriceSettingsModal() {
|
||||
document.getElementById('price-settings-modal')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
function applyPriceSettings() {
|
||||
const estimateVal = parseInt(document.getElementById('settings-pricelist-estimate')?.value || '');
|
||||
const warehouseVal = parseInt(document.getElementById('settings-pricelist-warehouse')?.value || '');
|
||||
const competitorVal = parseInt(document.getElementById('settings-pricelist-competitor')?.value || '');
|
||||
const disableVal = Boolean(document.getElementById('settings-disable-price-refresh')?.checked);
|
||||
|
||||
selectedPricelistIds.estimate = Number.isFinite(estimateVal) && estimateVal > 0 ? estimateVal : null;
|
||||
selectedPricelistIds.warehouse = Number.isFinite(warehouseVal) && warehouseVal > 0 ? warehouseVal : null;
|
||||
selectedPricelistIds.competitor = Number.isFinite(competitorVal) && competitorVal > 0 ? competitorVal : null;
|
||||
disablePriceRefresh = disableVal;
|
||||
|
||||
updateRefreshPricesButtonState();
|
||||
renderPricelistSettingsSummary();
|
||||
persistLocalPriceSettings();
|
||||
closePriceSettingsModal();
|
||||
|
||||
refreshPriceLevels().then(() => {
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
triggerAutoSave();
|
||||
});
|
||||
}
|
||||
|
||||
function getCategoryFromLotName(lotName) {
|
||||
@@ -482,7 +799,7 @@ function renderSingleSelectTab(categories) {
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase w-24">Тип</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
|
||||
<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 w-24">Цена</th>
|
||||
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-24">Estimate</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase w-20">Кол-во</th>
|
||||
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-28">Стоимость</th>
|
||||
<th class="px-3 py-2 w-10"></th>
|
||||
@@ -499,8 +816,9 @@ function renderSingleSelectTab(categories) {
|
||||
|
||||
const comp = selectedItem ? allComponents.find(c => c.lot_name === selectedItem.lot_name) : null;
|
||||
const price = comp?.current_price || 0;
|
||||
const estimate = selectedItem?.estimate_price ?? price;
|
||||
const qty = selectedItem?.quantity || 1;
|
||||
const total = price * qty;
|
||||
const total = (selectedItem ? getDisplayPrice(selectedItem) : price) * qty;
|
||||
|
||||
html += `
|
||||
<tr class="hover:bg-gray-50">
|
||||
@@ -518,14 +836,14 @@ function renderSingleSelectTab(categories) {
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-500 truncate max-w-xs" id="desc-${cat}">${escapeHtml(comp?.description || '')}</td>
|
||||
<td class="px-3 py-2 text-sm text-right" id="price-${cat}">${price ? '$' + price.toFixed(2) : '—'}</td>
|
||||
<td class="px-3 py-2 text-sm text-right" id="price-${cat}">${formatPriceOrNA(estimate)}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<input type="number" min="1" value="${qty}"
|
||||
id="qty-${cat}"
|
||||
onchange="updateSingleQuantity('${cat}', this.value)"
|
||||
class="w-16 px-2 py-1 border rounded text-center text-sm">
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-right font-medium" id="total-${cat}">${total ? '$' + total.toFixed(2) : '—'}</td>
|
||||
<td class="px-3 py-2 text-sm text-right font-medium" id="total-${cat}">${total ? formatMoney(total) : '—'}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
${selectedItem ? `
|
||||
<button onclick="clearSingleSelect('${cat}')" class="text-red-500 hover:text-red-700">
|
||||
@@ -557,7 +875,7 @@ function renderMultiSelectTab(components) {
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
|
||||
<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 w-24">Цена</th>
|
||||
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-24">Estimate</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase w-20">Кол-во</th>
|
||||
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-28">Стоимость</th>
|
||||
<th class="px-3 py-2 w-10"></th>
|
||||
@@ -569,19 +887,19 @@ function renderMultiSelectTab(components) {
|
||||
// Render existing cart items for this tab
|
||||
tabItems.forEach((item, idx) => {
|
||||
const comp = allComponents.find(c => c.lot_name === item.lot_name);
|
||||
const total = item.unit_price * item.quantity;
|
||||
const total = getDisplayPrice(item) * item.quantity;
|
||||
|
||||
html += `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-3 py-2 text-sm font-mono">${escapeHtml(item.lot_name)}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-500 truncate max-w-xs">${escapeHtml(item.description || comp?.description || '')}</td>
|
||||
<td class="px-3 py-2 text-sm text-right">$${item.unit_price.toFixed(2)}</td>
|
||||
<td class="px-3 py-2 text-sm text-right">${formatPriceOrNA(item.estimate_price ?? item.unit_price)}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<input type="number" min="1" value="${item.quantity}"
|
||||
onchange="updateMultiQuantity('${item.lot_name}', this.value)"
|
||||
class="w-16 px-2 py-1 border rounded text-center text-sm">
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-right font-medium">$${total.toFixed(2)}</td>
|
||||
<td class="px-3 py-2 text-sm text-right font-medium">${formatMoney(total)}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<button onclick="removeFromCart('${item.lot_name}')" class="text-red-500 hover:text-red-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -607,7 +925,7 @@ function renderMultiSelectTab(components) {
|
||||
onkeydown="handleAutocompleteKeyMulti(event)">
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-right text-gray-400" id="new-price">—</td>
|
||||
<td class="px-3 py-2 text-sm text-right text-gray-400" id="new-price">N/A</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<input type="number" min="1" value="1" id="new-qty"
|
||||
class="w-16 px-2 py-1 border rounded text-center text-sm">
|
||||
@@ -658,7 +976,7 @@ function renderMultiSelectTabWithSections(sections) {
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
|
||||
<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 w-24">Цена</th>
|
||||
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-24">Estimate</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase w-20">Кол-во</th>
|
||||
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-28">Стоимость</th>
|
||||
<th class="px-3 py-2 w-10"></th>
|
||||
@@ -670,19 +988,19 @@ function renderMultiSelectTabWithSections(sections) {
|
||||
// Render existing cart items for this section
|
||||
sectionItems.forEach((item) => {
|
||||
const comp = allComponents.find(c => c.lot_name === item.lot_name);
|
||||
const total = item.unit_price * item.quantity;
|
||||
const total = getDisplayPrice(item) * item.quantity;
|
||||
|
||||
html += `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-3 py-2 text-sm font-mono">${escapeHtml(item.lot_name)}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-500 truncate max-w-xs">${escapeHtml(item.description || comp?.description || '')}</td>
|
||||
<td class="px-3 py-2 text-sm text-right">$${item.unit_price.toFixed(2)}</td>
|
||||
<td class="px-3 py-2 text-sm text-right">${formatPriceOrNA(item.estimate_price ?? item.unit_price)}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<input type="number" min="1" value="${item.quantity}"
|
||||
onchange="updateMultiQuantity('${item.lot_name}', this.value)"
|
||||
class="w-16 px-2 py-1 border rounded text-center text-sm">
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-right font-medium">$${total.toFixed(2)}</td>
|
||||
<td class="px-3 py-2 text-sm text-right font-medium">${formatMoney(total)}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<button onclick="removeFromCart('${item.lot_name}')" class="text-red-500 hover:text-red-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -711,7 +1029,7 @@ function renderMultiSelectTabWithSections(sections) {
|
||||
onkeydown="handleAutocompleteKeySection(event, '${sectionId}')">
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-right text-gray-400" id="new-price-${sectionId}">—</td>
|
||||
<td class="px-3 py-2 text-sm text-right text-gray-400" id="new-price-${sectionId}">N/A</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<input type="number" min="1" value="1" id="new-qty-${sectionId}"
|
||||
class="w-16 px-2 py-1 border rounded text-center text-sm">
|
||||
@@ -833,14 +1151,26 @@ function selectAutocompleteItem(index) {
|
||||
lot_name: comp.lot_name,
|
||||
quantity: qty,
|
||||
unit_price: comp.current_price,
|
||||
estimate_price: comp.current_price,
|
||||
warehouse_price: null,
|
||||
competitor_price: null,
|
||||
delta_wh_estimate_abs: null,
|
||||
delta_wh_estimate_pct: null,
|
||||
delta_comp_estimate_abs: null,
|
||||
delta_comp_estimate_pct: null,
|
||||
delta_comp_wh_abs: null,
|
||||
delta_comp_wh_pct: null,
|
||||
price_missing: ['warehouse', 'competitor'],
|
||||
description: comp.description || '',
|
||||
category: getComponentCategory(comp)
|
||||
});
|
||||
|
||||
hideAutocomplete();
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
triggerAutoSave();
|
||||
refreshPriceLevels().then(() => {
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
triggerAutoSave();
|
||||
});
|
||||
}
|
||||
|
||||
function hideAutocomplete() {
|
||||
@@ -913,14 +1243,26 @@ function selectAutocompleteItemMulti(index) {
|
||||
lot_name: comp.lot_name,
|
||||
quantity: qty,
|
||||
unit_price: comp.current_price,
|
||||
estimate_price: comp.current_price,
|
||||
warehouse_price: null,
|
||||
competitor_price: null,
|
||||
delta_wh_estimate_abs: null,
|
||||
delta_wh_estimate_pct: null,
|
||||
delta_comp_estimate_abs: null,
|
||||
delta_comp_estimate_pct: null,
|
||||
delta_comp_wh_abs: null,
|
||||
delta_comp_wh_pct: null,
|
||||
price_missing: ['warehouse', 'competitor'],
|
||||
description: comp.description || '',
|
||||
category: getComponentCategory(comp)
|
||||
});
|
||||
|
||||
hideAutocomplete();
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
triggerAutoSave();
|
||||
refreshPriceLevels().then(() => {
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
triggerAutoSave();
|
||||
});
|
||||
}
|
||||
|
||||
// Autocomplete for sectioned tabs (like storage with RAID and Disks sections)
|
||||
@@ -1000,6 +1342,16 @@ function selectAutocompleteItemSection(index, sectionId) {
|
||||
lot_name: comp.lot_name,
|
||||
quantity: qty,
|
||||
unit_price: comp.current_price,
|
||||
estimate_price: comp.current_price,
|
||||
warehouse_price: null,
|
||||
competitor_price: null,
|
||||
delta_wh_estimate_abs: null,
|
||||
delta_wh_estimate_pct: null,
|
||||
delta_comp_estimate_abs: null,
|
||||
delta_comp_estimate_pct: null,
|
||||
delta_comp_wh_abs: null,
|
||||
delta_comp_wh_pct: null,
|
||||
price_missing: ['warehouse', 'competitor'],
|
||||
description: comp.description || '',
|
||||
category: getComponentCategory(comp)
|
||||
});
|
||||
@@ -1013,9 +1365,11 @@ function selectAutocompleteItemSection(index, sectionId) {
|
||||
// Reset quantity to 1
|
||||
if (qtyInput) qtyInput.value = '1';
|
||||
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
triggerAutoSave();
|
||||
refreshPriceLevels().then(() => {
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
triggerAutoSave();
|
||||
});
|
||||
}
|
||||
|
||||
function clearSingleSelect(category) {
|
||||
@@ -1054,7 +1408,7 @@ function updateMultiQuantity(lotName, value) {
|
||||
if (row) {
|
||||
const totalCell = row.querySelector('td:nth-child(5)');
|
||||
if (totalCell) {
|
||||
totalCell.textContent = '$' + (item.unit_price * item.quantity).toFixed(2);
|
||||
totalCell.textContent = formatMoney(getDisplayPrice(item) * item.quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1068,11 +1422,12 @@ function removeFromCart(lotName) {
|
||||
}
|
||||
|
||||
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});
|
||||
const total = cart.reduce((sum, item) => sum + (getDisplayPrice(item) * item.quantity), 0);
|
||||
document.getElementById('cart-total').textContent = formatMoney(total);
|
||||
|
||||
// Recalculate custom price section if active
|
||||
calculateCustomPrice();
|
||||
renderSalePriceTable();
|
||||
|
||||
if (cart.length === 0) {
|
||||
document.getElementById('cart-items').innerHTML =
|
||||
@@ -1116,7 +1471,7 @@ function updateCartUI() {
|
||||
html += `<div class="mb-2"><div class="text-xs font-medium text-gray-400 uppercase mb-1">${tabLabel}</div>`;
|
||||
|
||||
items.forEach(item => {
|
||||
const itemTotal = item.unit_price * item.quantity;
|
||||
const itemTotal = getDisplayPrice(item) * item.quantity;
|
||||
html += `
|
||||
<div class="flex justify-between items-center py-1 text-sm">
|
||||
<div class="flex-1">
|
||||
@@ -1125,7 +1480,7 @@ function updateCartUI() {
|
||||
<span>${item.quantity}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>$${itemTotal.toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
|
||||
<span>${formatMoney(itemTotal)}</span>
|
||||
<button onclick="removeFromCart('${item.lot_name}')" class="text-red-500 hover:text-red-700">
|
||||
<svg class="w-4 h-4" 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>
|
||||
@@ -1183,7 +1538,7 @@ async function saveConfig(showNotification = true) {
|
||||
custom_price: customPrice,
|
||||
notes: '',
|
||||
server_count: serverCountValue,
|
||||
pricelist_id: selectedPricelistId
|
||||
pricelist_id: selectedPricelistIds.estimate
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1226,12 +1581,143 @@ async function exportCSV() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatLineTotalTooltip(qty, unitPrice) {
|
||||
if (typeof unitPrice !== 'number' || unitPrice <= 0) return '';
|
||||
const lineTotal = qty * unitPrice;
|
||||
return `${formatNumberRu(qty)} * ${formatMoney(unitPrice)} = ${formatMoney(lineTotal)}`;
|
||||
}
|
||||
|
||||
function toggleSection(contentId, iconId) {
|
||||
const content = document.getElementById(contentId);
|
||||
const icon = document.getElementById(iconId);
|
||||
if (!content || !icon) return;
|
||||
|
||||
const isHidden = content.classList.toggle('hidden');
|
||||
if (isHidden) {
|
||||
icon.classList.add('-rotate-90');
|
||||
} else {
|
||||
icon.classList.remove('-rotate-90');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCartSummarySection() {
|
||||
toggleSection('cart-summary-content', 'cart-summary-toggle-icon');
|
||||
}
|
||||
|
||||
function toggleCustomPriceSection() {
|
||||
toggleSection('custom-price-content', 'custom-price-toggle-icon');
|
||||
}
|
||||
|
||||
function toggleSalePriceSection() {
|
||||
toggleSection('sale-price-content', 'sale-price-toggle-icon');
|
||||
}
|
||||
|
||||
function formatDiffPercent(baseTotal, compareTotal, compareLabel) {
|
||||
if (typeof baseTotal !== 'number' || typeof compareTotal !== 'number' || compareTotal <= 0) {
|
||||
return `N/A от ${compareLabel}`;
|
||||
}
|
||||
const pct = ((baseTotal - compareTotal) / compareTotal) * 100;
|
||||
const sign = pct > 0 ? '+' : '';
|
||||
return `${sign}${pct.toFixed(1)}% от ${compareLabel}`;
|
||||
}
|
||||
|
||||
function getTotalClass(current, references) {
|
||||
const validRefs = references.filter(v => typeof v === 'number' && v > 0);
|
||||
if (typeof current !== 'number' || current <= 0 || validRefs.length === 0) {
|
||||
return 'text-gray-900';
|
||||
}
|
||||
const avg = validRefs.reduce((sum, v) => sum + v, 0) / validRefs.length;
|
||||
if (current < avg) return 'text-green-600';
|
||||
if (current > avg) return 'text-red-600';
|
||||
return 'text-gray-900';
|
||||
}
|
||||
|
||||
function renderSalePriceTable() {
|
||||
const body = document.getElementById('sale-prices-body');
|
||||
const totalEstEl = document.getElementById('sale-total-est');
|
||||
const totalWarehouseEl = document.getElementById('sale-total-warehouse');
|
||||
const totalCompetitorEl = document.getElementById('sale-total-competitor');
|
||||
if (!body || !totalEstEl || !totalWarehouseEl || !totalCompetitorEl) return;
|
||||
|
||||
if (cart.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="5" class="px-3 py-3 text-center text-gray-500">Конфигурация пуста</td></tr>';
|
||||
totalEstEl.textContent = '$0.00';
|
||||
totalWarehouseEl.textContent = '$0.00';
|
||||
totalCompetitorEl.textContent = '$0.00';
|
||||
totalEstEl.title = '';
|
||||
totalWarehouseEl.title = '';
|
||||
totalCompetitorEl.title = '';
|
||||
totalEstEl.className = 'px-3 py-2 text-right';
|
||||
totalWarehouseEl.className = 'px-3 py-2 text-right';
|
||||
totalCompetitorEl.className = 'px-3 py-2 text-right';
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedCart = [...cart].sort((a, b) => {
|
||||
const catA = (a.category || getCategoryFromLotName(a.lot_name)).toUpperCase();
|
||||
const catB = (b.category || getCategoryFromLotName(b.lot_name)).toUpperCase();
|
||||
const orderA = categoryOrderMap[catA] || 9999;
|
||||
const orderB = categoryOrderMap[catB] || 9999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
let html = '';
|
||||
let totalEstimate = 0;
|
||||
let totalWarehouse = 0;
|
||||
let totalCompetitor = 0;
|
||||
|
||||
sortedCart.forEach(item => {
|
||||
const qty = item.quantity || 1;
|
||||
const estPrice = item.estimate_price;
|
||||
const warehousePrice = item.warehouse_price;
|
||||
const competitorPrice = item.competitor_price;
|
||||
|
||||
if (typeof estPrice === 'number' && estPrice > 0) totalEstimate += estPrice * qty;
|
||||
if (typeof warehousePrice === 'number' && warehousePrice > 0) totalWarehouse += warehousePrice * qty;
|
||||
if (typeof competitorPrice === 'number' && competitorPrice > 0) totalCompetitor += competitorPrice * qty;
|
||||
|
||||
const estTooltip = formatLineTotalTooltip(qty, estPrice);
|
||||
const warehouseTooltip = formatLineTotalTooltip(qty, warehousePrice);
|
||||
const competitorTooltip = formatLineTotalTooltip(qty, competitorPrice);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td class="px-3 py-2 font-mono">${escapeHtml(item.lot_name)}</td>
|
||||
<td class="px-3 py-2 text-right">${qty}</td>
|
||||
<td class="px-3 py-2 text-right" title="${escapeHtml(estTooltip)}">${formatPriceOrNA(estPrice)}</td>
|
||||
<td class="px-3 py-2 text-right" title="${escapeHtml(warehouseTooltip)}">${formatPriceOrNA(warehousePrice)}</td>
|
||||
<td class="px-3 py-2 text-right" title="${escapeHtml(competitorTooltip)}">${formatPriceOrNA(competitorPrice)}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
body.innerHTML = html;
|
||||
|
||||
totalEstEl.textContent = formatMoney(totalEstimate);
|
||||
totalWarehouseEl.textContent = formatMoney(totalWarehouse);
|
||||
totalCompetitorEl.textContent = formatMoney(totalCompetitor);
|
||||
|
||||
totalEstEl.title =
|
||||
`${formatDiffPercent(totalEstimate, totalWarehouse, 'склад')}\n` +
|
||||
`${formatDiffPercent(totalEstimate, totalCompetitor, 'конкуренты')}`;
|
||||
totalWarehouseEl.title =
|
||||
`${formatDiffPercent(totalWarehouse, totalEstimate, 'est.price')}\n` +
|
||||
`${formatDiffPercent(totalWarehouse, totalCompetitor, 'конкуренты')}`;
|
||||
totalCompetitorEl.title =
|
||||
`${formatDiffPercent(totalCompetitor, totalEstimate, 'est.price')}\n` +
|
||||
`${formatDiffPercent(totalCompetitor, totalWarehouse, 'склад')}`;
|
||||
|
||||
totalEstEl.className = `px-3 py-2 text-right ${getTotalClass(totalEstimate, [totalWarehouse, totalCompetitor])}`;
|
||||
totalWarehouseEl.className = `px-3 py-2 text-right ${getTotalClass(totalWarehouse, [totalEstimate, totalCompetitor])}`;
|
||||
totalCompetitorEl.className = `px-3 py-2 text-right ${getTotalClass(totalCompetitor, [totalEstimate, totalWarehouse])}`;
|
||||
}
|
||||
|
||||
// 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);
|
||||
const originalTotal = cart.reduce((sum, item) => sum + (getDisplayPrice(item) * item.quantity), 0);
|
||||
|
||||
if (customPrice <= 0 || cart.length === 0 || originalTotal <= 0) {
|
||||
document.getElementById('adjusted-prices').classList.add('hidden');
|
||||
@@ -1272,7 +1758,7 @@ function calculateCustomPrice() {
|
||||
let totalNew = 0;
|
||||
|
||||
sortedCart.forEach(item => {
|
||||
const originalPrice = item.unit_price;
|
||||
const originalPrice = getDisplayPrice(item);
|
||||
const newPrice = originalPrice * coefficient;
|
||||
const itemOriginalTotal = originalPrice * item.quantity;
|
||||
const itemNewTotal = newPrice * item.quantity;
|
||||
@@ -1284,17 +1770,17 @@ function calculateCustomPrice() {
|
||||
<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>
|
||||
<td class="px-3 py-2 text-right text-gray-500">${formatMoney(originalPrice)}</td>
|
||||
<td class="px-3 py-2 text-right text-green-600">${formatMoney(newPrice)}</td>
|
||||
<td class="px-3 py-2 text-right">${formatMoney(itemNewTotal)}</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-total-original').textContent = formatMoney(totalOriginal);
|
||||
document.getElementById('adjusted-total-new').textContent = formatMoney(totalNew);
|
||||
document.getElementById('adjusted-total-final').textContent = formatMoney(totalNew);
|
||||
document.getElementById('adjusted-prices').classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -1309,7 +1795,7 @@ 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);
|
||||
const originalTotal = cart.reduce((sum, item) => sum + (getDisplayPrice(item) * item.quantity), 0);
|
||||
|
||||
if (customPrice <= 0 || originalTotal <= 0) {
|
||||
showToast('Введите целевую цену', 'error');
|
||||
@@ -1321,7 +1807,7 @@ async function exportCSVWithCustomPrice() {
|
||||
// Create adjusted cart
|
||||
const adjustedCart = cart.map(item => ({
|
||||
...item,
|
||||
unit_price: parseFloat((item.unit_price * coefficient).toFixed(2))
|
||||
unit_price: parseFloat((getDisplayPrice(item) * coefficient).toFixed(2))
|
||||
}));
|
||||
|
||||
try {
|
||||
@@ -1346,6 +1832,10 @@ async function exportCSVWithCustomPrice() {
|
||||
async function refreshPrices() {
|
||||
// RBAC disabled - no token check required
|
||||
if (!configUUID) return;
|
||||
if (disablePriceRefresh) {
|
||||
showToast('Обновление цен отключено в настройках', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/configs/' + configUUID + '/refresh-prices', {
|
||||
@@ -1368,6 +1858,9 @@ async function refreshPrices() {
|
||||
lot_name: item.lot_name,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unit_price,
|
||||
estimate_price: item.unit_price,
|
||||
warehouse_price: null,
|
||||
competitor_price: null,
|
||||
description: item.description || '',
|
||||
category: item.category || getCategoryFromLotName(item.lot_name)
|
||||
}));
|
||||
@@ -1378,18 +1871,17 @@ async function refreshPrices() {
|
||||
updatePriceUpdateDate(config.price_updated_at);
|
||||
}
|
||||
if (config.pricelist_id) {
|
||||
selectedPricelistId = config.pricelist_id;
|
||||
const select = document.getElementById('pricelist-select');
|
||||
if (select) {
|
||||
const hasOption = Array.from(select.options).some(opt => Number(opt.value) === Number(selectedPricelistId));
|
||||
if (!hasOption) {
|
||||
await loadActivePricelists();
|
||||
}
|
||||
select.value = String(selectedPricelistId);
|
||||
selectedPricelistIds.estimate = config.pricelist_id;
|
||||
if (!activePricelistsBySource.estimate.some(opt => Number(opt.id) === Number(config.pricelist_id))) {
|
||||
await loadActivePricelists();
|
||||
}
|
||||
syncPriceSettingsControls();
|
||||
renderPricelistSettingsSummary();
|
||||
persistLocalPriceSettings();
|
||||
}
|
||||
|
||||
// Re-render UI
|
||||
await refreshPriceLevels();
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user