feat: add СХД configuration type with storage-specific tabs and LOT catalog guide
- Add config_type field ("server"|"storage") to Configuration and LocalConfiguration
- Create modal: Сервер/СХД segmented control in configs.html and project_detail.html
- Configurator: ENC/DKC/CTL categories in Base tab, HIC section in PCI tab hidden for server configs
- Add SW tab (categories: SW) to configurator, visible only when components present
- TAB_CONFIG.pci: add HIC section for storage HIC adapters (separate from server HBA/NIC)
- Migration 029: ALTER TABLE qt_configurations ADD COLUMN config_type
- Fix: skip Error 1833 (Cannot change column used in FK) in GORM AutoMigrate
- Operator guide: docs/storage-components-guide.md with LOT naming rules and DE4000H catalog template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,19 @@
|
||||
<h2 class="text-xl font-semibold mb-4">Новая конфигурация</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Тип оборудования</label>
|
||||
<div class="inline-flex rounded-lg border border-gray-200 overflow-hidden w-full">
|
||||
<button type="button" id="type-server-btn" onclick="setCreateType('server')"
|
||||
class="flex-1 py-2 text-sm font-medium bg-blue-600 text-white">
|
||||
Сервер
|
||||
</button>
|
||||
<button type="button" id="type-storage-btn" onclick="setCreateType('storage')"
|
||||
class="flex-1 py-2 text-sm font-medium bg-white text-gray-700 hover:bg-gray-50 border-l border-gray-200">
|
||||
СХД
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Название конфигурации</label>
|
||||
<input type="text" id="opportunity-number" placeholder="Например: Сервер для проекта X"
|
||||
@@ -518,7 +531,19 @@ async function cloneConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
let createConfigType = 'server';
|
||||
|
||||
function setCreateType(type) {
|
||||
createConfigType = type;
|
||||
document.getElementById('type-server-btn').className = 'flex-1 py-2 text-sm font-medium ' +
|
||||
(type === 'server' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50 border-l border-gray-200');
|
||||
document.getElementById('type-storage-btn').className = 'flex-1 py-2 text-sm font-medium border-l border-gray-200 ' +
|
||||
(type === 'storage' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50');
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
createConfigType = 'server';
|
||||
setCreateType('server');
|
||||
document.getElementById('opportunity-number').value = '';
|
||||
document.getElementById('create-project-input').value = '';
|
||||
document.getElementById('create-modal').classList.remove('hidden');
|
||||
@@ -573,7 +598,8 @@ async function createConfigWithProject(name, projectUUID) {
|
||||
items: [],
|
||||
notes: '',
|
||||
server_count: 1,
|
||||
project_uuid: projectUUID || null
|
||||
project_uuid: projectUUID || null,
|
||||
config_type: createConfigType
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
|
||||
Accessories
|
||||
</button>
|
||||
<button onclick="switchTab('sw')" data-tab="sw"
|
||||
class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
|
||||
SW
|
||||
</button>
|
||||
<button onclick="switchTab('other')" data-tab="other"
|
||||
class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 whitespace-nowrap">
|
||||
Other
|
||||
@@ -365,7 +369,7 @@
|
||||
// Tab configuration - will be populated dynamically
|
||||
let TAB_CONFIG = {
|
||||
base: {
|
||||
categories: ['MB', 'CPU', 'MEM'],
|
||||
categories: ['MB', 'CPU', 'MEM', 'ENC', 'DKC', 'CTL'],
|
||||
singleSelect: true,
|
||||
label: 'Base'
|
||||
},
|
||||
@@ -379,13 +383,14 @@ let TAB_CONFIG = {
|
||||
]
|
||||
},
|
||||
pci: {
|
||||
categories: ['GPU', 'DPU', 'NIC', 'HCA', 'HBA'],
|
||||
categories: ['GPU', 'DPU', 'NIC', 'HCA', 'HBA', 'HIC'],
|
||||
singleSelect: false,
|
||||
label: 'PCI',
|
||||
sections: [
|
||||
{ title: 'GPU / DPU', categories: ['GPU', 'DPU'] },
|
||||
{ title: 'NIC / HCA', categories: ['NIC', 'HCA'] },
|
||||
{ title: 'HBA', categories: ['HBA'] }
|
||||
{ title: 'HBA', categories: ['HBA'] },
|
||||
{ title: 'HIC', categories: ['HIC'] }
|
||||
]
|
||||
},
|
||||
power: {
|
||||
@@ -398,6 +403,11 @@ let TAB_CONFIG = {
|
||||
singleSelect: false,
|
||||
label: 'Accessories'
|
||||
},
|
||||
sw: {
|
||||
categories: ['SW'],
|
||||
singleSelect: false,
|
||||
label: 'SW'
|
||||
},
|
||||
other: {
|
||||
categories: [],
|
||||
singleSelect: false,
|
||||
@@ -411,6 +421,7 @@ let ASSIGNED_CATEGORIES = Object.values(TAB_CONFIG)
|
||||
|
||||
// State
|
||||
let configUUID = '{{.ConfigUUID}}';
|
||||
let configType = 'server';
|
||||
let configName = '';
|
||||
let currentVersionNo = 1;
|
||||
let projectUUID = '';
|
||||
@@ -793,6 +804,8 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
|
||||
const config = await resp.json();
|
||||
configName = config.name;
|
||||
configType = config.config_type || 'server';
|
||||
applyConfigTypeToTabs();
|
||||
currentVersionNo = config.current_version_no || 1;
|
||||
projectUUID = config.project_uuid || '';
|
||||
await loadProjectIndex();
|
||||
@@ -1139,6 +1152,45 @@ function switchTab(tab) {
|
||||
renderTab();
|
||||
}
|
||||
|
||||
const ALWAYS_VISIBLE_TABS = new Set(['base', 'storage', 'pci']);
|
||||
|
||||
// Storage-only categories — hidden for server configs
|
||||
const STORAGE_ONLY_BASE_CATEGORIES = ['ENC', 'DKC', 'CTL'];
|
||||
|
||||
function applyConfigTypeToTabs() {
|
||||
if (configType === 'storage') return; // storage sees everything
|
||||
// Remove ENC/DKC/CTL from Base
|
||||
TAB_CONFIG.base.categories = TAB_CONFIG.base.categories.filter(
|
||||
c => !STORAGE_ONLY_BASE_CATEGORIES.includes(c)
|
||||
);
|
||||
// Remove HIC from PCI tab
|
||||
TAB_CONFIG.pci.categories = TAB_CONFIG.pci.categories.filter(c => c !== 'HIC');
|
||||
TAB_CONFIG.pci.sections = TAB_CONFIG.pci.sections.filter(s => s.title !== 'HIC');
|
||||
// Rebuild assigned categories index
|
||||
ASSIGNED_CATEGORIES = Object.values(TAB_CONFIG)
|
||||
.flatMap(t => t.categories)
|
||||
.map(c => c.toUpperCase());
|
||||
}
|
||||
|
||||
function updateTabVisibility() {
|
||||
for (const tabId of Object.keys(TAB_CONFIG)) {
|
||||
if (ALWAYS_VISIBLE_TABS.has(tabId)) continue;
|
||||
const btn = document.querySelector(`[data-tab="${tabId}"]`);
|
||||
if (!btn) continue;
|
||||
const hasComponents = getComponentsForTab(tabId).length > 0;
|
||||
const hasCartItems = cart.some(item => {
|
||||
const cat = (item.category || getCategoryFromLotName(item.lot_name) || '').toUpperCase();
|
||||
return getTabForCategory(cat) === tabId;
|
||||
});
|
||||
const visible = hasComponents || hasCartItems;
|
||||
btn.classList.toggle('hidden', !visible);
|
||||
// If the current tab just got hidden, fall back to base
|
||||
if (!visible && currentTab === tabId) {
|
||||
switchTab('base');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getComponentsForTab(tab) {
|
||||
const config = TAB_CONFIG[tab];
|
||||
return allComponents.filter(comp => {
|
||||
@@ -1870,12 +1922,14 @@ function updateMultiQuantity(lotName, value) {
|
||||
|
||||
function removeFromCart(lotName) {
|
||||
cart = cart.filter(i => i.lot_name !== lotName);
|
||||
updateTabVisibility();
|
||||
renderTab();
|
||||
updateCartUI();
|
||||
triggerAutoSave();
|
||||
}
|
||||
|
||||
function updateCartUI() {
|
||||
updateTabVisibility();
|
||||
window._currentCart = cart; // expose for BOM/Pricing tabs
|
||||
const total = cart.reduce((sum, item) => sum + (getDisplayPrice(item) * item.quantity), 0);
|
||||
document.getElementById('cart-total').textContent = formatMoney(total);
|
||||
|
||||
@@ -77,6 +77,19 @@
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Новая конфигурация в проекте</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Тип оборудования</label>
|
||||
<div class="inline-flex rounded-lg border border-gray-200 overflow-hidden w-full">
|
||||
<button type="button" id="pd-type-server-btn" onclick="pdSetCreateType('server')"
|
||||
class="flex-1 py-2 text-sm font-medium bg-blue-600 text-white">
|
||||
Сервер
|
||||
</button>
|
||||
<button type="button" id="pd-type-storage-btn" onclick="pdSetCreateType('storage')"
|
||||
class="flex-1 py-2 text-sm font-medium bg-white text-gray-700 hover:bg-gray-50 border-l border-gray-200">
|
||||
СХД
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Название конфигурации</label>
|
||||
<input type="text" id="create-name" placeholder="Например: OPP-2026-001"
|
||||
@@ -576,7 +589,19 @@ async function loadConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
let pdCreateConfigType = 'server';
|
||||
|
||||
function pdSetCreateType(type) {
|
||||
pdCreateConfigType = type;
|
||||
document.getElementById('pd-type-server-btn').className = 'flex-1 py-2 text-sm font-medium ' +
|
||||
(type === 'server' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50');
|
||||
document.getElementById('pd-type-storage-btn').className = 'flex-1 py-2 text-sm font-medium border-l border-gray-200 ' +
|
||||
(type === 'storage' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50');
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
pdCreateConfigType = 'server';
|
||||
pdSetCreateType('server');
|
||||
document.getElementById('create-name').value = '';
|
||||
document.getElementById('create-modal').classList.remove('hidden');
|
||||
document.getElementById('create-modal').classList.add('flex');
|
||||
@@ -934,7 +959,7 @@ async function createConfig() {
|
||||
const resp = await fetch('/api/projects/' + projectUUID + '/configs', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name: name, items: [], notes: '', server_count: 1})
|
||||
body: JSON.stringify({name: name, items: [], notes: '', server_count: 1, config_type: pdCreateConfigType})
|
||||
});
|
||||
if (!resp.ok) {
|
||||
alert('Не удалось создать конфигурацию');
|
||||
|
||||
Reference in New Issue
Block a user