Persist pricing state and refresh storage sync

This commit is contained in:
Mikhail Chusavitin
2026-04-15 18:56:40 +03:00
parent 7e1e2ac18d
commit 1137c6d4db
2 changed files with 153 additions and 63 deletions

View File

@@ -378,7 +378,6 @@ let TAB_CONFIG = {
singleSelect: false,
label: 'Storage',
sections: [
{ title: 'RAID Контроллеры', categories: ['RAID'] },
{ title: 'Диски', categories: ['M2', 'SSD', 'HDD', 'EDSFF', 'HHHL'] }
]
},
@@ -837,6 +836,7 @@ document.addEventListener('DOMContentLoaded', async function() {
serverModelForQuote = config.server_model || '';
supportCode = config.support_code || '';
currentArticle = config.article || '';
restorePricingStateFromNotes(config.notes || '');
// Restore custom price if saved
if (config.custom_price) {
@@ -1158,9 +1158,16 @@ const ALWAYS_VISIBLE_TABS = new Set(['base', 'storage', 'pci']);
const STORAGE_ONLY_BASE_CATEGORIES = ['DKC', 'CTL', 'ENC'];
// Server-only categories — hidden for storage configs
const SERVER_ONLY_BASE_CATEGORIES = ['MB', 'CPU', 'MEM'];
const STORAGE_HIDDEN_STORAGE_CATEGORIES = ['RAID'];
const STORAGE_HIDDEN_PCI_CATEGORIES = ['GPU', 'DPU'];
const STORAGE_HIDDEN_POWER_CATEGORIES = ['PS', 'PSU'];
function applyConfigTypeToTabs() {
const baseCategories = ['MB', 'CPU', 'MEM', 'DKC', 'CTL', 'ENC'];
const storageCategories = ['RAID', 'M2', 'SSD', 'HDD', 'EDSFF', 'HHHL'];
const storageSections = [
{ title: 'Диски', categories: ['M2', 'SSD', 'HDD', 'EDSFF', 'HHHL'] }
];
const pciCategories = ['GPU', 'DPU', 'NIC', 'HCA', 'HBA', 'HIC'];
const pciSections = [
{ title: 'GPU / DPU', categories: ['GPU', 'DPU'] },
@@ -1168,6 +1175,7 @@ function applyConfigTypeToTabs() {
{ title: 'HBA', categories: ['HBA'] },
{ title: 'HIC', categories: ['HIC'] }
];
const powerCategories = ['PS', 'PSU'];
TAB_CONFIG.base.categories = baseCategories.filter(c => {
if (configType === 'storage') {
@@ -1176,11 +1184,22 @@ function applyConfigTypeToTabs() {
return !STORAGE_ONLY_BASE_CATEGORIES.includes(c);
});
TAB_CONFIG.storage.categories = storageCategories.filter(c => {
return configType === 'storage' ? !STORAGE_HIDDEN_STORAGE_CATEGORIES.includes(c) : true;
});
TAB_CONFIG.storage.sections = storageSections;
TAB_CONFIG.pci.categories = pciCategories.filter(c => {
return configType === 'storage' ? true : c !== 'HIC';
return configType === 'storage' ? !STORAGE_HIDDEN_PCI_CATEGORIES.includes(c) : c !== 'HIC';
});
TAB_CONFIG.pci.sections = pciSections.filter(section => {
return configType === 'storage' ? true : section.title !== 'HIC';
if (configType === 'storage') {
return !section.categories.every(cat => STORAGE_HIDDEN_PCI_CATEGORIES.includes(cat));
}
return section.title !== 'HIC';
});
TAB_CONFIG.power.categories = powerCategories.filter(c => {
return configType === 'storage' ? !STORAGE_HIDDEN_POWER_CATEGORIES.includes(c) : true;
});
// Rebuild assigned categories index
@@ -1243,7 +1262,7 @@ function renderSingleSelectTab(categories) {
if (currentTab === 'base') {
html += `
<div class="mb-1 grid grid-cols-1 md:grid-cols-[1fr,16rem] gap-3 items-start">
<label for="server-model-input" class="block text-sm font-medium text-gray-700">Модель сервера для КП:</label>
<label for="server-model-input" class="block text-sm font-medium text-gray-700">Модель системы для партномера:</label>
<label for="support-code-select" class="block text-sm font-medium text-gray-700">Уровень техподдержки:</label>
</div>
<div class="mb-3 grid grid-cols-1 md:grid-cols-[1fr,16rem] gap-3 items-start">
@@ -2096,6 +2115,58 @@ function getCurrentArticle() {
return currentArticle || '';
}
function buildPricingState() {
const buyCustom = parseDecimalInput(document.getElementById('pricing-custom-price-buy')?.value || '');
const saleUplift = parseDecimalInput(document.getElementById('pricing-uplift-sale')?.value || '');
const saleCustom = parseDecimalInput(document.getElementById('pricing-custom-price-sale')?.value || '');
return {
buy_custom_price: buyCustom > 0 ? buyCustom : null,
sale_uplift: saleUplift > 0 ? saleUplift : null,
sale_custom_price: saleCustom > 0 ? saleCustom : null,
};
}
function serializeConfigNotes() {
return JSON.stringify({
pricing_ui: buildPricingState()
});
}
function restorePricingStateFromNotes(notesRaw) {
if (!notesRaw) return;
let parsed;
try {
parsed = JSON.parse(notesRaw);
} catch (_) {
return;
}
const pricing = parsed?.pricing_ui;
if (!pricing || typeof pricing !== 'object') return;
const buyInput = document.getElementById('pricing-custom-price-buy');
if (buyInput) {
buyInput.value = typeof pricing.buy_custom_price === 'number' && pricing.buy_custom_price > 0
? pricing.buy_custom_price.toFixed(2)
: '';
}
const upliftInput = document.getElementById('pricing-uplift-sale');
if (upliftInput) {
upliftInput.value = typeof pricing.sale_uplift === 'number' && pricing.sale_uplift > 0
? formatUpliftInput(pricing.sale_uplift)
: '';
}
const saleInput = document.getElementById('pricing-custom-price-sale');
if (saleInput) {
saleInput.value = typeof pricing.sale_custom_price === 'number' && pricing.sale_custom_price > 0
? pricing.sale_custom_price.toFixed(2)
: '';
}
}
function getAutosaveStorageKey() {
return `qf_config_autosave_${configUUID || 'default'}`;
}
@@ -2109,7 +2180,7 @@ function buildSavePayload() {
name: configName,
items: cart,
custom_price: customPrice,
notes: '',
notes: serializeConfigNotes(),
server_count: serverCount,
server_model: serverModelForQuote,
support_code: supportCode,
@@ -2588,66 +2659,67 @@ async function refreshPrices() {
return;
}
const refreshBtn = document.getElementById('refresh-prices-btn');
const previousLabel = refreshBtn ? refreshBtn.textContent : '';
try {
const refreshPayload = {};
if (selectedPricelistIds.estimate) {
refreshPayload.pricelist_id = selectedPricelistIds.estimate;
if (refreshBtn) {
refreshBtn.disabled = true;
refreshBtn.textContent = 'Обновление...';
refreshBtn.className = 'px-4 py-2 bg-gray-300 text-gray-500 rounded cursor-not-allowed';
}
const resp = await fetch('/api/configs/' + configUUID + '/refresh-prices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(refreshPayload)
const componentSyncResp = await fetch('/api/sync/components', { method: 'POST' });
if (!componentSyncResp.ok) {
throw new Error('component sync failed');
}
const pricelistSyncResp = await fetch('/api/sync/pricelists', { method: 'POST' });
if (!pricelistSyncResp.ok) {
throw new Error('pricelist sync failed');
}
await Promise.all([
loadActivePricelists(true),
loadAllComponents()
]);
['estimate', 'warehouse', 'competitor'].forEach(source => {
const latest = activePricelistsBySource[source]?.[0];
if (latest && latest.id) {
selectedPricelistIds[source] = Number(latest.id);
resolvedAutoPricelistIds[source] = null;
}
});
if (!resp.ok) {
showToast('Ошибка обновления цен', 'error');
return;
}
syncPriceSettingsControls();
renderPricelistSettingsSummary();
persistLocalPriceSettings();
const config = await resp.json();
// Update cart with new prices
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)
}));
}
// Update price update date
if (config.price_updated_at) {
updatePriceUpdateDate(config.price_updated_at);
}
if (config.pricelist_id) {
if (selectedPricelistIds.estimate) {
selectedPricelistIds.estimate = config.pricelist_id;
} else {
resolvedAutoPricelistIds.estimate = Number(config.pricelist_id);
}
if (!activePricelistsBySource.estimate.some(opt => Number(opt.id) === Number(config.pricelist_id))) {
await loadActivePricelists();
}
syncPriceSettingsControls();
renderPricelistSettingsSummary();
if (selectedPricelistIds.estimate) {
persistLocalPriceSettings();
}
}
// Re-render UI
await saveConfig(false);
await refreshPriceLevels({ force: true, noCache: true });
renderTab();
updateCartUI();
if (configUUID) {
const configResp = await fetch('/api/configs/' + configUUID);
if (configResp.ok) {
const config = await configResp.json();
if (config.price_updated_at) {
updatePriceUpdateDate(config.price_updated_at);
}
}
}
showToast('Цены обновлены', 'success');
} catch(e) {
showToast('Ошибка обновления цен', 'error');
} finally {
if (refreshBtn) {
refreshBtn.disabled = false;
refreshBtn.textContent = previousLabel || 'Обновить цены';
updateRefreshPricesButtonState();
}
}
}
@@ -4019,14 +4091,17 @@ function applyCustomPrice(table) {
function onBuyCustomPriceInput() {
applyCustomPrice('buy');
triggerAutoSave();
}
function onSaleCustomPriceInput() {
applyCustomPrice('sale');
triggerAutoSave();
}
function onSaleMarkupInput() {
renderPricingTab();
triggerAutoSave();
}
function setPricingCustomPriceFromVendor() {