fix: complete solution for instant customization storage display
Major changes: 1. Reverted to using pooled storage grid for instant customization 2. Parse storage requirements from server description 3. Pre-fill pooled storage with correct drives (4x 4TB + 2x 3.2TB) 4. Original drives marked as €0 (included in base price) 5. Removed individual slot display which was causing issues How it works: - Parse "4x Crucial T705 4TB NVMe + 2x Kioxia CM7-V 3.2TB NVMe" - Match drives by capacity, brand, and model - Pre-fill the pooled storage grid - Storage summary shows correct counts - Price stays at €1520 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4251,7 +4251,8 @@ var wpcf7 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render options with the exact WHMCS preselected values
|
// Render options with the exact WHMCS preselected values
|
||||||
renderDynamicOptions(options, true, preselected);
|
// Also pass the storage requirements for proper pre-filling
|
||||||
|
renderDynamicOptions(options, true, preselected, storageReqs);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
renderFallbackOptions(container);
|
renderFallbackOptions(container);
|
||||||
@@ -4606,7 +4607,7 @@ var wpcf7 = {
|
|||||||
updateSummary();
|
updateSummary();
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderDynamicOptions(options, isInstant = false, preselected = {}) {
|
function renderDynamicOptions(options, isInstant = false, preselected = {}, storageRequirements = []) {
|
||||||
const container = isInstant ? document.getElementById('instantDynamicConfigContainer') : document.getElementById('dynamicConfigContainer');
|
const container = isInstant ? document.getElementById('instantDynamicConfigContainer') : document.getElementById('dynamicConfigContainer');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
@@ -4643,107 +4644,71 @@ var wpcf7 = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For instant customization, don't group - show each slot individually
|
// For both instant and custom, group storage options by signature
|
||||||
// For custom servers, group similar slots together
|
// This creates a pooled grid where we can prefill with the correct drives
|
||||||
if (isInstant) {
|
const storageGroups = {};
|
||||||
// Show each storage slot individually
|
const groupOrder = []; // Preserve order
|
||||||
groups.storage.forEach((opt, index) => {
|
|
||||||
const label = document.createElement('div');
|
|
||||||
label.className = 'section-label';
|
|
||||||
label.style.marginTop = '1.5rem';
|
|
||||||
label.textContent = `💿 ${opt.label}`;
|
|
||||||
container.appendChild(label);
|
|
||||||
|
|
||||||
// Create visualizer for this single slot
|
groups.storage.forEach(opt => {
|
||||||
const visualContainer = document.createElement('div');
|
// Create a signature based on available values
|
||||||
visualContainer.className = 'storage-slots-container';
|
const signature = opt.values.map(v => v.text.trim().toLowerCase() + '|' + v.price).join('||');
|
||||||
const uniqueSuffix = '-instant-s' + index;
|
|
||||||
|
|
||||||
visualContainer.innerHTML = `
|
if (!storageGroups[signature]) {
|
||||||
<div class="slots-header">
|
storageGroups[signature] = [];
|
||||||
<span>Slot: <strong>${opt.label}</strong></span>
|
groupOrder.push(signature);
|
||||||
<span id="dynamic-slots-status${uniqueSuffix}" style="color:var(--success); font-size:0.75rem;">Configured</span>
|
}
|
||||||
</div>
|
storageGroups[signature].push(opt);
|
||||||
<div class="slots-visual" id="dynamic-slots-visual${uniqueSuffix}">
|
});
|
||||||
<div class="drive-slot filled"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(visualContainer);
|
|
||||||
|
|
||||||
// Create individual option grid for this slot
|
// Render each group
|
||||||
// For instant customization, we need to pass the specific preselected value for this slot
|
groupOrder.forEach((sig, groupIndex) => {
|
||||||
const slotPreselected = {};
|
const groupOpts = storageGroups[sig];
|
||||||
if (isInstant && preselected[opt.id]) {
|
const firstOpt = groupOpts[0];
|
||||||
slotPreselected[opt.id] = preselected[opt.id];
|
|
||||||
}
|
|
||||||
container.appendChild(createOptionGrid(opt, true, -1, isInstant, slotPreselected));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Group storage options by signature for custom servers
|
|
||||||
const storageGroups = {};
|
|
||||||
const groupOrder = []; // Preserve order
|
|
||||||
|
|
||||||
groups.storage.forEach(opt => {
|
const label = document.createElement('div');
|
||||||
// Create a signature based on available values
|
label.className = 'section-label';
|
||||||
const signature = opt.values.map(v => v.text.trim().toLowerCase() + '|' + v.price).join('||');
|
label.style.marginTop = '1.5rem';
|
||||||
|
|
||||||
if (!storageGroups[signature]) {
|
// Detect Title based on content
|
||||||
storageGroups[signature] = [];
|
const firstLabel = firstOpt.label.toLowerCase();
|
||||||
groupOrder.push(signature);
|
// Check values for keywords
|
||||||
}
|
const hasGen5 = firstOpt.values.some(v => v.text.toLowerCase().includes('gen5'));
|
||||||
storageGroups[signature].push(opt);
|
const hasGen4 = firstOpt.values.some(v => v.text.toLowerCase().includes('gen4'));
|
||||||
});
|
const hasEnt = firstOpt.values.some(v => v.text.toLowerCase().includes('enterprise') || v.text.toLowerCase().includes('ent'));
|
||||||
|
|
||||||
// Render each group
|
let title = '💿 STORAGE CONFIGURATION';
|
||||||
groupOrder.forEach((sig, groupIndex) => {
|
if (hasGen5) title = '🚀 GEN5 NVME STORAGE (ENTERPRISE)';
|
||||||
const groupOpts = storageGroups[sig];
|
else if (hasGen4) title = '💿 GEN4 NVME STORAGE (CONSUMER)';
|
||||||
const firstOpt = groupOpts[0];
|
else if (hasEnt) title = '💾 ENTERPRISE STORAGE';
|
||||||
|
|
||||||
const label = document.createElement('div');
|
// Append Group Index if multiple groups exist to differentiate
|
||||||
label.className = 'section-label';
|
if (groupOrder.length > 1) {
|
||||||
label.style.marginTop = '1.5rem';
|
title += ` (Group ${groupIndex + 1})`;
|
||||||
|
}
|
||||||
|
|
||||||
// Detect Title based on content
|
label.textContent = title;
|
||||||
const firstLabel = firstOpt.label.toLowerCase();
|
container.appendChild(label);
|
||||||
// Check values for keywords
|
|
||||||
const hasGen5 = firstOpt.values.some(v => v.text.toLowerCase().includes('gen5'));
|
|
||||||
const hasGen4 = firstOpt.values.some(v => v.text.toLowerCase().includes('gen4'));
|
|
||||||
const hasEnt = firstOpt.values.some(v => v.text.toLowerCase().includes('enterprise') || v.text.toLowerCase().includes('ent'));
|
|
||||||
|
|
||||||
let title = '💿 STORAGE CONFIGURATION';
|
// Visualizer for THIS group
|
||||||
if (hasGen5) title = '🚀 GEN5 NVME STORAGE (ENTERPRISE)';
|
const maxSlots = groupOpts.length;
|
||||||
else if (hasGen4) title = '💿 GEN4 NVME STORAGE (CONSUMER)';
|
const visualContainer = document.createElement('div');
|
||||||
else if (hasEnt) title = '💾 ENTERPRISE STORAGE';
|
visualContainer.className = 'storage-slots-container';
|
||||||
|
const uniqueSuffix = (isInstant ? '-instant' : '') + '-g' + groupIndex;
|
||||||
|
|
||||||
// Append Group Index if multiple groups exist to differentiate
|
visualContainer.innerHTML = `
|
||||||
if (groupOrder.length > 1) {
|
<div class="slots-header">
|
||||||
title += ` (Group ${groupIndex + 1})`;
|
<span>Available Slots: <strong id="dynamic-slots-count${uniqueSuffix}">0/${maxSlots}</strong></span>
|
||||||
}
|
<span id="dynamic-slots-status${uniqueSuffix}" style="color:var(--success); font-size:0.75rem;">Select drives</span>
|
||||||
|
</div>
|
||||||
|
<div class="slots-visual" id="dynamic-slots-visual${uniqueSuffix}">
|
||||||
|
${Array(maxSlots).fill('<div class="drive-slot"></div>').join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(visualContainer);
|
||||||
|
|
||||||
label.textContent = title;
|
// Always use pooled grid for these groups as they are by definition identical
|
||||||
container.appendChild(label);
|
container.appendChild(createPooledStorageGrid(groupOpts, isInstant, preselected, groupIndex, storageRequirements));
|
||||||
|
});
|
||||||
// Visualizer for THIS group
|
|
||||||
const maxSlots = groupOpts.length;
|
|
||||||
const visualContainer = document.createElement('div');
|
|
||||||
visualContainer.className = 'storage-slots-container';
|
|
||||||
const uniqueSuffix = (isInstant ? '-instant' : '') + '-g' + groupIndex;
|
|
||||||
|
|
||||||
visualContainer.innerHTML = `
|
|
||||||
<div class="slots-header">
|
|
||||||
<span>Available Slots: <strong id="dynamic-slots-count${uniqueSuffix}">0/${maxSlots}</strong></span>
|
|
||||||
<span id="dynamic-slots-status${uniqueSuffix}" style="color:var(--success); font-size:0.75rem;">Select drives</span>
|
|
||||||
</div>
|
|
||||||
<div class="slots-visual" id="dynamic-slots-visual${uniqueSuffix}">
|
|
||||||
${Array(maxSlots).fill('<div class="drive-slot"></div>').join('')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(visualContainer);
|
|
||||||
|
|
||||||
// Always use pooled grid for these groups as they are by definition identical
|
|
||||||
container.appendChild(createPooledStorageGrid(groupOpts, isInstant, preselected, groupIndex));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Network & Other
|
// 3. Network & Other
|
||||||
@@ -4769,7 +4734,7 @@ var wpcf7 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New Pooled Storage Grid
|
// New Pooled Storage Grid
|
||||||
function createPooledStorageGrid(storageOptions, isInstant, preselected = {}, groupIndex = 0) {
|
function createPooledStorageGrid(storageOptions, isInstant, preselected = {}, groupIndex = 0, storageRequirements = []) {
|
||||||
const grid = document.createElement('div');
|
const grid = document.createElement('div');
|
||||||
grid.className = 'config-grid';
|
grid.className = 'config-grid';
|
||||||
|
|
||||||
@@ -4800,29 +4765,72 @@ var wpcf7 = {
|
|||||||
|
|
||||||
window.pooledState[poolKey] = { slots: initializedSlots };
|
window.pooledState[poolKey] = { slots: initializedSlots };
|
||||||
|
|
||||||
// For instant customization, track original drives
|
// For instant customization, track original drives and pre-fill with requirements
|
||||||
if (isInstant) {
|
if (isInstant) {
|
||||||
window.originalDrives = [];
|
window.originalDrives = [];
|
||||||
|
|
||||||
|
// First reset all slots to None
|
||||||
initializedSlots.forEach(slot => {
|
initializedSlots.forEach(slot => {
|
||||||
// Track the preselected drives as "original"
|
const noneOption = slot.values.find(v =>
|
||||||
if (preselected[slot.id] && slot.currentVal.text !== '-' &&
|
v.text.trim() === '-' ||
|
||||||
!slot.currentVal.text.toLowerCase().includes('none')) {
|
v.text.toLowerCase().includes('none') ||
|
||||||
window.originalDrives.push({
|
v.text.toLowerCase().includes('no hard drive')
|
||||||
slotId: slot.id,
|
);
|
||||||
driveId: slot.currentVal.id,
|
if (noneOption) {
|
||||||
text: slot.currentVal.text,
|
slot.currentVal = noneOption;
|
||||||
price: slot.currentVal.price
|
configState[slot.label] = noneOption.price;
|
||||||
});
|
configIds[slot.id] = noneOption.id;
|
||||||
|
|
||||||
// Original drives have no additional cost (included in base price)
|
|
||||||
configState[slot.label] = 0;
|
|
||||||
} else {
|
|
||||||
configState[slot.label] = slot.currentVal.price;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configIds[slot.id] = slot.currentVal.id;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fill slots based on storage requirements
|
||||||
|
if (storageRequirements && storageRequirements.length > 0) {
|
||||||
|
storageRequirements.forEach(req => {
|
||||||
|
for (let i = 0; i < req.qty; i++) {
|
||||||
|
// Find an empty slot
|
||||||
|
const emptySlot = initializedSlots.find(s =>
|
||||||
|
s.currentVal.text === '-' ||
|
||||||
|
s.currentVal.text.toLowerCase().includes('none')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emptySlot) {
|
||||||
|
// Find matching drive type
|
||||||
|
const match = emptySlot.values.find(v => {
|
||||||
|
const vText = v.text.toLowerCase();
|
||||||
|
const reqSpec = req.spec.toLowerCase();
|
||||||
|
|
||||||
|
// Extract capacity
|
||||||
|
const valCap = vText.match(/(\d+(?:\.\d+)?)\s*tb/i);
|
||||||
|
const reqCap = reqSpec.match(/(\d+(?:\.\d+)?)\s*tb/i);
|
||||||
|
if (valCap && reqCap && valCap[1] === reqCap[1]) {
|
||||||
|
// Check brand match
|
||||||
|
if ((vText.includes('crucial') && reqSpec.includes('crucial')) ||
|
||||||
|
(vText.includes('kioxia') && reqSpec.includes('kioxia')) ||
|
||||||
|
(vText.includes('t705') && reqSpec.includes('t705')) ||
|
||||||
|
(vText.includes('cm7') && reqSpec.includes('cm7'))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
emptySlot.currentVal = match;
|
||||||
|
configState[emptySlot.label] = 0; // Included in base price
|
||||||
|
configIds[emptySlot.id] = match.id;
|
||||||
|
|
||||||
|
// Track as original drive
|
||||||
|
window.originalDrives.push({
|
||||||
|
slotId: emptySlot.id,
|
||||||
|
driveId: match.id,
|
||||||
|
text: match.text,
|
||||||
|
price: match.price
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Pre-set configState for custom servers
|
// Pre-set configState for custom servers
|
||||||
initializedSlots.forEach(slot => {
|
initializedSlots.forEach(slot => {
|
||||||
|
|||||||
Reference in New Issue
Block a user