Deduplicate configuration revisions and update revisions UI

This commit is contained in:
2026-02-19 14:09:00 +03:00
parent 71f73e2f1d
commit cbaeafa9c8
10 changed files with 839 additions and 188 deletions

View File

@@ -401,18 +401,28 @@ function updateConfigBreadcrumbs() {
}
codeEl.textContent = code;
variantEl.textContent = variant;
configEl.textContent = configName || 'Конфигурация';
const fullConfigName = configName || 'Конфигурация';
configEl.textContent = truncateBreadcrumbSpecName(fullConfigName);
configEl.title = fullConfigName;
versionEl.textContent = 'v' + (currentVersionNo || 1);
const configNameLinkEl = document.getElementById('breadcrumb-config-name-link');
if (configNameLinkEl && configUUID) {
configNameLinkEl.href = '/configs/' + configUUID + '/revisions';
}
}
function truncateBreadcrumbSpecName(name) {
const maxLength = 16;
if (!name || name.length <= maxLength) return name;
return name.slice(0, maxLength - 1) + '…';
}
let currentTab = 'base';
let allComponents = [];
let cart = [];
let categoryOrderMap = {}; // Category code -> display_order mapping
let autoSaveTimeout = null; // Timeout for debounced autosave
let hasUnsavedChanges = false;
let exitSaveStarted = false;
let serverCount = 1; // Server count for the configuration
let serverModelForQuote = '';
let supportCode = '';
@@ -1890,13 +1900,128 @@ function getCurrentArticle() {
return currentArticle || '';
}
function getAutosaveStorageKey() {
return `qf_config_autosave_${configUUID || 'default'}`;
}
function buildSavePayload() {
const customPriceInput = document.getElementById('custom-price-input');
const customPriceValue = parseFloat(customPriceInput.value);
const customPrice = customPriceValue > 0 ? customPriceValue : null;
return {
name: configName,
items: cart,
custom_price: customPrice,
notes: '',
server_count: serverCount,
server_model: serverModelForQuote,
support_code: supportCode,
article: getCurrentArticle(),
pricelist_id: selectedPricelistIds.estimate,
only_in_stock: onlyInStock
};
}
function persistAutosaveDraft() {
if (!configUUID) return;
try {
sessionStorage.setItem(getAutosaveStorageKey(), JSON.stringify({
payload: buildSavePayload(),
saved_at: Date.now()
}));
} catch (_) {
// ignore storage failures
}
}
function clearAutosaveDraft() {
try {
sessionStorage.removeItem(getAutosaveStorageKey());
} catch (_) {
// ignore storage failures
}
}
function restoreAutosaveDraftIfAny() {
if (!configUUID) return;
let raw = null;
try {
raw = sessionStorage.getItem(getAutosaveStorageKey());
} catch (_) {
raw = null;
}
if (!raw) return;
try {
const parsed = JSON.parse(raw);
const payload = parsed && parsed.payload ? parsed.payload : null;
if (!payload) return;
if (Array.isArray(payload.items)) {
cart = payload.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)
}));
}
if (typeof payload.server_count === 'number' && payload.server_count > 0) {
serverCount = payload.server_count;
const serverCountInput = document.getElementById('server-count');
if (serverCountInput) serverCountInput.value = serverCount;
const totalServerCount = document.getElementById('total-server-count');
if (totalServerCount) totalServerCount.textContent = serverCount;
}
serverModelForQuote = payload.server_model || serverModelForQuote;
supportCode = payload.support_code || supportCode;
currentArticle = payload.article || currentArticle;
selectedPricelistIds.estimate = payload.pricelist_id || selectedPricelistIds.estimate;
onlyInStock = Boolean(payload.only_in_stock);
const customPriceInput = document.getElementById('custom-price-input');
if (customPriceInput) {
if (typeof payload.custom_price === 'number' && payload.custom_price > 0) {
customPriceInput.value = payload.custom_price.toFixed(2);
} else {
customPriceInput.value = '';
}
}
hasUnsavedChanges = true;
} catch (_) {
// ignore invalid draft
}
}
function saveConfigOnExit() {
if (!configUUID || !hasUnsavedChanges || exitSaveStarted) return;
exitSaveStarted = true;
try {
fetch('/api/configs/' + configUUID, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(buildSavePayload()),
keepalive: true
});
} catch (_) {
// best effort save on page exit
}
}
function triggerAutoSave() {
// Debounce autosave - wait 1 second after last change
// Autosave keeps local draft only; server revision is created on Save/Exit.
hasUnsavedChanges = true;
if (autoSaveTimeout) {
clearTimeout(autoSaveTimeout);
}
autoSaveTimeout = setTimeout(() => {
saveConfig(false); // false = don't show notification
persistAutosaveDraft();
}, 1000);
}
@@ -1910,32 +2035,13 @@ async function saveConfig(showNotification = true) {
await refreshPriceLevels({ force: true, noCache: true });
// Get custom price if set
const customPriceInput = document.getElementById('custom-price-input');
const customPriceValue = parseFloat(customPriceInput.value);
const customPrice = customPriceValue > 0 ? customPriceValue : null;
// Get server count
const serverCountValue = serverCount;
try {
const resp = await fetch('/api/configs/' + configUUID, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: configName,
items: cart,
custom_price: customPrice,
notes: '',
server_count: serverCountValue,
server_model: serverModelForQuote,
support_code: supportCode,
article: getCurrentArticle(),
pricelist_id: selectedPricelistIds.estimate,
only_in_stock: onlyInStock
})
body: JSON.stringify(buildSavePayload())
});
if (!resp.ok) {
@@ -1951,6 +2057,9 @@ async function saveConfig(showNotification = true) {
const versionEl = document.getElementById('breadcrumb-config-version');
if (versionEl) versionEl.textContent = 'v' + currentVersionNo;
}
hasUnsavedChanges = false;
clearAutosaveDraft();
exitSaveStarted = false;
if (showNotification) {
showToast('Сохранено', 'success');