fix: use exact WHMCS configoption IDs for instant customization
Instead of parsing storage strings and trying to match: - Parse configoption IDs directly from WHMCS order URL - Use these exact IDs to preselect components - For storage, use the preselected values in the pooled grid - Original drives marked as €0 (included in base price) This ensures the customization shows exactly what WHMCS has configured: - 4x Crucial T705 4TB + 2x Kioxia CM7-V 3.2TB - Not trying to guess or parse storage descriptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4181,153 +4181,30 @@ var wpcf7 = {
|
||||
});
|
||||
}
|
||||
|
||||
// Parse Storage Requirements - More robust parsing
|
||||
let storageRequirements = [];
|
||||
if (selectedServer.storage) {
|
||||
const parts = selectedServer.storage.split('+');
|
||||
parts.forEach(part => {
|
||||
const trimmed = part.trim();
|
||||
if (!trimmed) return;
|
||||
// For instant customization, use the exact preselected IDs from WHMCS URL
|
||||
options.forEach(opt => {
|
||||
let selectedValId = preselected[opt.id]; // Use the ID directly from WHMCS
|
||||
|
||||
// Match patterns like "2x 1TB NVMe" or "1TB NVMe"
|
||||
const match = trimmed.match(/^(\d+)x\s+(.+)$/);
|
||||
if (match) {
|
||||
storageRequirements.push({
|
||||
qty: parseInt(match[1]),
|
||||
spec: match[2].trim().toLowerCase()
|
||||
// Validate that the preselected ID exists in the options
|
||||
if (selectedValId && !opt.values.find(v => v.id === selectedValId)) {
|
||||
console.warn(`Invalid preselected ID ${selectedValId} for option ${opt.id}`);
|
||||
selectedValId = null;
|
||||
}
|
||||
|
||||
// For non-storage options, apply price adjustment
|
||||
if (opt.type !== 'storage' && selectedValId) {
|
||||
const selectedVal = opt.values.find(v => v.id === selectedValId);
|
||||
if (selectedVal && selectedVal.price > 0) {
|
||||
const offset = selectedVal.price;
|
||||
opt.values.forEach(v => {
|
||||
v.price = parseFloat((v.price - offset).toFixed(2));
|
||||
});
|
||||
} else {
|
||||
storageRequirements.push({
|
||||
qty: 1,
|
||||
spec: trimmed.toLowerCase()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For storage options, we'll handle them in the pooled grid
|
||||
// So we skip individual storage matching here
|
||||
const storageOptions = options.filter(opt => opt.type === 'storage');
|
||||
const nonStorageOptions = options.filter(opt => opt.type !== 'storage');
|
||||
|
||||
// Enhanced matching for non-storage instant server specs
|
||||
nonStorageOptions.forEach(opt => {
|
||||
let selectedValId = null;
|
||||
let urlId = preselected[opt.id];
|
||||
|
||||
// Enhanced RAM matching
|
||||
if (opt.type === 'ram') {
|
||||
const ramMatch = selectedServer.ram.match(/(\d+)\s*GB/i);
|
||||
const ramAmount = ramMatch ? parseInt(ramMatch[1]) : 0;
|
||||
|
||||
// Find the closest RAM option
|
||||
let bestMatch = null;
|
||||
let smallestDiff = Infinity;
|
||||
|
||||
opt.values.forEach(val => {
|
||||
const valMatch = val.text.match(/(\d+)\s*GB/i);
|
||||
const valAmount = valMatch ? parseInt(valMatch[1]) : 0;
|
||||
|
||||
if (valAmount === ramAmount) {
|
||||
bestMatch = val.id;
|
||||
smallestDiff = 0;
|
||||
} else if (Math.abs(valAmount - ramAmount) < smallestDiff) {
|
||||
bestMatch = val.id;
|
||||
smallestDiff = Math.abs(valAmount - ramAmount);
|
||||
}
|
||||
});
|
||||
|
||||
selectedValId = bestMatch;
|
||||
}
|
||||
// Enhanced network matching
|
||||
else if (opt.type === 'network') {
|
||||
const networkSpec = selectedServer.network.toLowerCase();
|
||||
const match = opt.values.find(v => {
|
||||
const vText = v.text.toLowerCase();
|
||||
// Exact match first
|
||||
if (vText === networkSpec) return true;
|
||||
// Match bandwidth (e.g., "1gbit", "10gbit")
|
||||
const vSpeed = vText.match(/(\d+)\s*[gm]bit/);
|
||||
const sSpeed = networkSpec.match(/(\d+)\s*[gm]bit/);
|
||||
if (vSpeed && sSpeed && vSpeed[1] === sSpeed[1]) return true;
|
||||
return false;
|
||||
});
|
||||
selectedValId = match ? match.id : null;
|
||||
}
|
||||
// Location matching
|
||||
else if (opt.type === 'location') {
|
||||
const match = opt.values.find(v =>
|
||||
v.text.toLowerCase().includes(selectedServer.location.toLowerCase()) ||
|
||||
selectedServer.location.toLowerCase().includes(v.text.toLowerCase())
|
||||
);
|
||||
selectedValId = match ? match.id : null;
|
||||
}
|
||||
// Other component types
|
||||
else {
|
||||
// Try to find best match based on text similarity
|
||||
const serverSpec = selectedServer.cpu ? selectedServer.cpu.toLowerCase() : '';
|
||||
if (serverSpec) {
|
||||
const match = opt.values.find(v => {
|
||||
const vText = v.text.toLowerCase();
|
||||
// Look for key matching terms
|
||||
const serverTerms = serverSpec.split(/\s+/);
|
||||
const matchingTerms = serverTerms.filter(term =>
|
||||
term.length > 2 && vText.includes(term)
|
||||
);
|
||||
return matchingTerms.length > 0;
|
||||
});
|
||||
selectedValId = match ? match.id : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to URL ID if available and valid
|
||||
if (!selectedValId && urlId && opt.values.find(v => v.id === urlId)) {
|
||||
selectedValId = urlId;
|
||||
}
|
||||
|
||||
// Final fallback - try stale ID map
|
||||
if (!selectedValId && urlId && idToTextMap[urlId]) {
|
||||
const originalText = idToTextMap[urlId];
|
||||
const cleanTarget = originalText.replace(/\s?[€$£].*/, '').trim().toLowerCase();
|
||||
const match = opt.values.find(v =>
|
||||
v.text.toLowerCase().includes(cleanTarget) ||
|
||||
cleanTarget.includes(v.text.toLowerCase())
|
||||
);
|
||||
if (match) selectedValId = match.id;
|
||||
}
|
||||
|
||||
// Last resort - pick first option (avoid "None" if possible)
|
||||
if (!selectedValId && opt.values.length > 0) {
|
||||
const nonNoneOptions = opt.values.filter(v =>
|
||||
v.text.trim() !== '-' &&
|
||||
!v.text.toLowerCase().includes('none') &&
|
||||
!v.text.toLowerCase().includes('no hard drive')
|
||||
);
|
||||
selectedValId = nonNoneOptions.length > 0 ? nonNoneOptions[0].id : opt.values[0].id;
|
||||
}
|
||||
|
||||
// For storage options, we don't adjust prices as they're handled by pooled grid
|
||||
if (opt.type === 'storage') {
|
||||
// Don't adjust storage prices - let pooled grid handle it
|
||||
if (selectedValId) {
|
||||
preselected[opt.id] = selectedValId;
|
||||
}
|
||||
} else {
|
||||
// For non-storage options, apply price adjustment
|
||||
if (selectedValId) {
|
||||
preselected[opt.id] = selectedValId;
|
||||
const selectedVal = opt.values.find(v => v.id === selectedValId);
|
||||
if (selectedVal && selectedVal.price > 0) {
|
||||
const offset = selectedVal.price;
|
||||
opt.values.forEach(v => {
|
||||
v.price = parseFloat((v.price - offset).toFixed(2));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
renderDynamicOptions(options, true, preselected, storageRequirements);
|
||||
// Render options with the exact WHMCS preselected values
|
||||
renderDynamicOptions(options, true, preselected);
|
||||
|
||||
} else {
|
||||
renderFallbackOptions(container);
|
||||
@@ -4682,7 +4559,7 @@ var wpcf7 = {
|
||||
updateSummary();
|
||||
};
|
||||
|
||||
function renderDynamicOptions(options, isInstant = false, preselected = {}, storageRequirements = []) {
|
||||
function renderDynamicOptions(options, isInstant = false, preselected = {}) {
|
||||
const container = isInstant ? document.getElementById('instantDynamicConfigContainer') : document.getElementById('dynamicConfigContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
@@ -4784,7 +4661,7 @@ var wpcf7 = {
|
||||
container.appendChild(visualContainer);
|
||||
|
||||
// Always use pooled grid for these groups as they are by definition identical
|
||||
container.appendChild(createPooledStorageGrid(groupOpts, isInstant, preselected, groupIndex, storageRequirements));
|
||||
container.appendChild(createPooledStorageGrid(groupOpts, isInstant, preselected, groupIndex));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4811,7 +4688,7 @@ var wpcf7 = {
|
||||
}
|
||||
|
||||
// New Pooled Storage Grid
|
||||
function createPooledStorageGrid(storageOptions, isInstant, preselected = {}, groupIndex = 0, storageRequirements = []) {
|
||||
function createPooledStorageGrid(storageOptions, isInstant, preselected = {}, groupIndex = 0) {
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'config-grid';
|
||||
|
||||
@@ -4821,14 +4698,12 @@ var wpcf7 = {
|
||||
// Use a unique key for each group to prevent conflicts
|
||||
const poolKey = (isInstant ? 'instant' : 'custom') + '-g' + groupIndex;
|
||||
|
||||
// Storage requirements are now passed as a parameter for instant customization
|
||||
|
||||
// Initialize slots with preselected values or defaults
|
||||
// Initialize slots with preselected values from WHMCS or defaults
|
||||
const initializedSlots = storageOptions.map(opt => {
|
||||
const preId = preselected[opt.id];
|
||||
let initialVal = opt.values[0]; // Default
|
||||
let initialVal = opt.values[0]; // Default (usually None)
|
||||
|
||||
// 1. Try ID Match (Standard)
|
||||
// Use the preselected value from WHMCS if available
|
||||
if (preId) {
|
||||
const found = opt.values.find(v => v.id === preId);
|
||||
if (found) initialVal = found;
|
||||
@@ -4844,102 +4719,31 @@ var wpcf7 = {
|
||||
|
||||
window.pooledState[poolKey] = { slots: initializedSlots };
|
||||
|
||||
// For instant customization, pre-fill with the server's storage configuration
|
||||
if (isInstant && storageRequirements.length > 0) {
|
||||
// Track original drives to avoid double-charging
|
||||
// For instant customization, track original drives
|
||||
if (isInstant) {
|
||||
window.originalDrives = [];
|
||||
|
||||
// First, reset all slots to None
|
||||
initializedSlots.forEach(slot => {
|
||||
const noneOption = slot.values.find(v =>
|
||||
v.text.trim() === '-' ||
|
||||
v.text.toLowerCase().includes('none') ||
|
||||
v.text.toLowerCase().includes('no hard drive')
|
||||
);
|
||||
if (noneOption) {
|
||||
slot.currentVal = noneOption;
|
||||
configState[slot.label] = noneOption.price;
|
||||
configIds[slot.id] = noneOption.id;
|
||||
// Track the preselected drives as "original"
|
||||
if (preselected[slot.id] && slot.currentVal.text !== '-' &&
|
||||
!slot.currentVal.text.toLowerCase().includes('none')) {
|
||||
window.originalDrives.push({
|
||||
slotId: slot.id,
|
||||
driveId: slot.currentVal.id,
|
||||
text: slot.currentVal.text,
|
||||
price: slot.currentVal.price
|
||||
});
|
||||
|
||||
// Original drives have no additional cost (included in base price)
|
||||
configState[slot.label] = 0;
|
||||
} else {
|
||||
configState[slot.label] = slot.currentVal.price;
|
||||
}
|
||||
});
|
||||
|
||||
// Fill slots based on storage requirements
|
||||
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') ||
|
||||
s.currentVal.text.toLowerCase().includes('no hard drive')
|
||||
);
|
||||
|
||||
if (emptySlot) {
|
||||
// Find exact matching drive type
|
||||
const match = emptySlot.values.find(v => {
|
||||
const vText = v.text.toLowerCase();
|
||||
|
||||
// Parse both to normalized form for comparison
|
||||
const normalizeSpec = (spec) => {
|
||||
return spec.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/nvme/g, '')
|
||||
.replace(/ssd/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
// Extract capacity from value
|
||||
const valCapacityMatch = vText.match(/(\d+(?:\.\d+)?)(?:\s*tb)?/i);
|
||||
const valCapacity = valCapacityMatch ? parseFloat(valCapacityMatch[1]) : 0;
|
||||
|
||||
// Extract capacity from requirement
|
||||
const reqCapacityMatch = req.spec.match(/(\d+(?:\.\d+)?)(?:\s*tb)?/i);
|
||||
const reqCapacity = reqCapacityMatch ? parseFloat(reqCapacityMatch[1]) : 0;
|
||||
|
||||
// Match capacity exactly
|
||||
if (Math.abs(valCapacity - reqCapacity) < 0.1) {
|
||||
// Check for exact brand match first
|
||||
if (vText.includes('crucial') && req.spec.includes('crucial')) return true;
|
||||
if (vText.includes('kioxia') && req.spec.includes('kioxia')) return true;
|
||||
if (vText.includes('samsung') && req.spec.includes('samsung')) return true;
|
||||
if (vText.includes('solidigm') && req.spec.includes('solidigm')) return true;
|
||||
if (vText.includes('wd') && req.spec.includes('wd')) return true;
|
||||
if (vText.includes('seagate') && req.spec.includes('seagate')) return true;
|
||||
if (vText.includes('kingston') && req.spec.includes('kingston')) return true;
|
||||
|
||||
// If no brand match, check if it's just generic NVMe
|
||||
if (!req.spec.includes('crucial') && !req.spec.includes('kioxia') &&
|
||||
!req.spec.includes('samsung') && !req.spec.includes('solidigm')) {
|
||||
return vText.includes('nvme');
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (match) {
|
||||
emptySlot.currentVal = match;
|
||||
// For instant customization, original drives have no additional cost
|
||||
configState[emptySlot.label] = 0; // Price is 0 because it's included in base price
|
||||
configIds[emptySlot.id] = match.id;
|
||||
|
||||
// Track this as an original drive
|
||||
window.originalDrives.push({
|
||||
slotId: emptySlot.id,
|
||||
driveId: match.id,
|
||||
text: match.text,
|
||||
price: match.price
|
||||
});
|
||||
} else {
|
||||
console.warn(`Could not find matching drive for requirement: ${req.spec}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`No empty slots available for requirement: ${req.spec}`);
|
||||
}
|
||||
}
|
||||
configIds[slot.id] = slot.currentVal.id;
|
||||
});
|
||||
} else {
|
||||
// Pre-set configState for non-instant or fallback
|
||||
// Pre-set configState for custom servers
|
||||
initializedSlots.forEach(slot => {
|
||||
configState[slot.label] = slot.currentVal.price;
|
||||
configIds[slot.id] = slot.currentVal.id;
|
||||
|
||||
Reference in New Issue
Block a user