Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83a3202bdf | ||
|
|
4bc7979a70 | ||
|
|
1137c6d4db |
Binary file not shown.
@@ -28,8 +28,9 @@ type ComponentSyncResult struct {
|
||||
func (l *LocalDB) SyncComponents(mariaDB *gorm.DB) (*ComponentSyncResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Query to join lot with qt_lot_metadata (metadata only, no pricing)
|
||||
// Use LEFT JOIN to include lots without metadata
|
||||
// Build the component catalog from every runtime source of LOT names.
|
||||
// Storage lots may exist in qt_lot_metadata / qt_pricelist_items before they appear in lot,
|
||||
// so the sync cannot start from lot alone.
|
||||
type componentRow struct {
|
||||
LotName string
|
||||
LotDescription string
|
||||
@@ -40,15 +41,29 @@ func (l *LocalDB) SyncComponents(mariaDB *gorm.DB) (*ComponentSyncResult, error)
|
||||
var rows []componentRow
|
||||
err := mariaDB.Raw(`
|
||||
SELECT
|
||||
l.lot_name,
|
||||
l.lot_description,
|
||||
COALESCE(c.code, SUBSTRING_INDEX(l.lot_name, '_', 1)) as category,
|
||||
m.model
|
||||
FROM lot l
|
||||
LEFT JOIN qt_lot_metadata m ON l.lot_name = m.lot_name
|
||||
src.lot_name,
|
||||
COALESCE(MAX(NULLIF(TRIM(l.lot_description), '')), '') AS lot_description,
|
||||
COALESCE(
|
||||
MAX(NULLIF(TRIM(c.code), '')),
|
||||
MAX(NULLIF(TRIM(l.lot_category), '')),
|
||||
SUBSTRING_INDEX(src.lot_name, '_', 1)
|
||||
) AS category,
|
||||
MAX(NULLIF(TRIM(m.model), '')) AS model
|
||||
FROM (
|
||||
SELECT lot_name FROM lot
|
||||
UNION
|
||||
SELECT lot_name FROM qt_lot_metadata
|
||||
WHERE is_hidden = FALSE OR is_hidden IS NULL
|
||||
UNION
|
||||
SELECT lot_name FROM qt_pricelist_items
|
||||
) src
|
||||
LEFT JOIN lot l ON l.lot_name = src.lot_name
|
||||
LEFT JOIN qt_lot_metadata m
|
||||
ON m.lot_name = src.lot_name
|
||||
AND (m.is_hidden = FALSE OR m.is_hidden IS NULL)
|
||||
LEFT JOIN qt_categories c ON m.category_id = c.id
|
||||
WHERE m.is_hidden = FALSE OR m.is_hidden IS NULL
|
||||
ORDER BY l.lot_name
|
||||
GROUP BY src.lot_name
|
||||
ORDER BY src.lot_name
|
||||
`).Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying components from MariaDB: %w", err)
|
||||
|
||||
@@ -837,6 +837,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 +1159,17 @@ 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: 'RAID Контроллеры', categories: ['RAID'] },
|
||||
{ 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 +1177,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 +1186,27 @@ 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.filter(section => {
|
||||
if (configType === 'storage') {
|
||||
return !section.categories.every(cat => STORAGE_HIDDEN_STORAGE_CATEGORIES.includes(cat));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
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 +1269,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 +2122,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 +2187,7 @@ function buildSavePayload() {
|
||||
name: configName,
|
||||
items: cart,
|
||||
custom_price: customPrice,
|
||||
notes: '',
|
||||
notes: serializeConfigNotes(),
|
||||
server_count: serverCount,
|
||||
server_model: serverModelForQuote,
|
||||
support_code: supportCode,
|
||||
@@ -2588,66 +2666,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 +4098,17 @@ function applyCustomPrice(table) {
|
||||
|
||||
function onBuyCustomPriceInput() {
|
||||
applyCustomPrice('buy');
|
||||
triggerAutoSave();
|
||||
}
|
||||
|
||||
function onSaleCustomPriceInput() {
|
||||
applyCustomPrice('sale');
|
||||
triggerAutoSave();
|
||||
}
|
||||
|
||||
function onSaleMarkupInput() {
|
||||
renderPricingTab();
|
||||
triggerAutoSave();
|
||||
}
|
||||
|
||||
function setPricingCustomPriceFromVendor() {
|
||||
|
||||
Reference in New Issue
Block a user