fix: retain instant server specifications when customizing
- Enhanced component matching algorithm with scoring system - Improved storage matching with capacity, type, and brand detection - Pre-fill pooled storage grid with instant server configuration - Display correct quantities for preselected storage drives - Better RAM, network, and location matching When user clicks Customize on an instant server, the system now: 1. Analyzes the instant server specs (CPU, RAM, Storage, Network) 2. Intelligently matches them with available configuration options 3. Pre-selects the matching components in the configuration tool 4. Allows user to modify the pre-configured setup 5. Maintains all specifications in the WHMCS order URL 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4181,96 +4181,190 @@ var wpcf7 = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Parse Storage Requirements
|
// Parse Storage Requirements - More robust parsing
|
||||||
let storageRequirements = {};
|
let storageRequirements = [];
|
||||||
if (selectedServer.storage) {
|
if (selectedServer.storage) {
|
||||||
const parts = selectedServer.storage.split('+');
|
const parts = selectedServer.storage.split('+');
|
||||||
parts.forEach(part => {
|
parts.forEach(part => {
|
||||||
const match = part.trim().match(/^(\d+)x\s+(.+)$/);
|
const trimmed = part.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
|
||||||
|
// Match patterns like "2x 1TB NVMe" or "1TB NVMe"
|
||||||
|
const match = trimmed.match(/^(\d+)x\s+(.+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
storageRequirements[match[2].trim().toLowerCase()] = parseInt(match[1]);
|
storageRequirements.push({
|
||||||
} else if (part.trim().length > 0) {
|
qty: parseInt(match[1]),
|
||||||
storageRequirements[part.trim().toLowerCase()] = 1;
|
spec: match[2].trim().toLowerCase()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
storageRequirements.push({
|
||||||
|
qty: 1,
|
||||||
|
spec: trimmed.toLowerCase()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ADJUST PRICES relative to the Included Options
|
// Enhanced matching for instant server specs
|
||||||
options.forEach(opt => {
|
options.forEach(opt => {
|
||||||
let selectedValId = null;
|
let selectedValId = null;
|
||||||
let urlId = preselected[opt.id]; // Original URL ID
|
let urlId = preselected[opt.id];
|
||||||
|
|
||||||
// 1. PRIORITY: Plan Requirements (Text Description)
|
// Enhanced storage matching
|
||||||
// We try to fulfill the written plan specs first.
|
|
||||||
if (opt.type === 'storage') {
|
if (opt.type === 'storage') {
|
||||||
for (const [reqName, reqQty] of Object.entries(storageRequirements)) {
|
// Create a score for each option based on how well it matches the requirements
|
||||||
if (reqQty > 0) {
|
const scores = opt.values.map(val => {
|
||||||
const match = opt.values.find(v => {
|
let score = 0;
|
||||||
const vText = v.text.toLowerCase();
|
const valText = val.text.toLowerCase();
|
||||||
// Simple Robust Fuzzy Match
|
|
||||||
// Clean requirements (remove "nvme", "gen4" etc to focus on Brand + Capacity)
|
|
||||||
const cleanReq = reqName.toLowerCase().replace(/nvme/g, '').replace(/gen[45]/g, '').replace(/\s+/g, ' ').trim();
|
|
||||||
const cleanVal = vText.replace(/\s?\(.*?\)/, '').replace(/[€$£].*/, '').trim();
|
|
||||||
|
|
||||||
// Check if Value contains Requirement (e.g. "Crucial 1TB" contains "crucial 1tb")
|
// Check against each storage requirement
|
||||||
// OR if Requirement contains Value (e.g. "2x Crucial 1TB" contains "Crucial 1TB")
|
for (const req of storageRequirements) {
|
||||||
return cleanVal.includes(cleanReq) || cleanReq.includes(cleanVal);
|
// Extract capacity from value (e.g., "1tb", "2tb")
|
||||||
|
const valCapacityMatch = valText.match(/(\d+)\s*tb/i);
|
||||||
|
const valCapacity = valCapacityMatch ? parseInt(valCapacityMatch[1]) : 0;
|
||||||
|
|
||||||
|
// Extract capacity from requirement
|
||||||
|
const reqCapacityMatch = req.spec.match(/(\d+)\s*tb/i);
|
||||||
|
const reqCapacity = reqCapacityMatch ? parseInt(reqCapacityMatch[1]) : 0;
|
||||||
|
|
||||||
|
// Score based on capacity match
|
||||||
|
if (valCapacity === reqCapacity && reqCapacity > 0) {
|
||||||
|
score += 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score based on type keywords
|
||||||
|
const keywords = ['nvme', 'ssd', 'gen4', 'gen5', 'enterprise', 'consumer'];
|
||||||
|
keywords.forEach(keyword => {
|
||||||
|
if (valText.includes(keyword) && req.spec.includes(keyword)) {
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (match) {
|
// Score based on brand
|
||||||
selectedValId = match.id;
|
const brands = ['samsung', 'crucial', 'wd', 'seagate', 'kingston'];
|
||||||
storageRequirements[reqName]--;
|
brands.forEach(brand => {
|
||||||
// console.log(`Slot ${opt.label} matched requirement: ${reqName}`);
|
if (valText.includes(brand) && req.spec.includes(brand)) {
|
||||||
break;
|
score += 30;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bonus for exact text match
|
||||||
|
if (valText.includes(req.spec) || req.spec.includes(valText.replace(/\s?\(.*?\)/, '').trim())) {
|
||||||
|
score += 50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. FALLBACK: URL ID (if valid)
|
return { id: val.id, score };
|
||||||
// If we didn't match a requirement (or ran out), use the URL's choice.
|
});
|
||||||
if (!selectedValId && urlId) {
|
|
||||||
if (opt.values.find(v => v.id === urlId)) {
|
// Select the highest scoring option
|
||||||
|
scores.sort((a, b) => b.score - a.score);
|
||||||
|
if (scores[0] && scores[0].score > 0) {
|
||||||
|
selectedValId = scores[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no good match, try to use "None" option
|
||||||
|
if (!selectedValId) {
|
||||||
|
const noneOpt = opt.values.find(v =>
|
||||||
|
v.text.trim() === '-' ||
|
||||||
|
v.text.toLowerCase().includes('none') ||
|
||||||
|
v.text.toLowerCase().includes('no hard drive')
|
||||||
|
);
|
||||||
|
selectedValId = noneOpt ? noneOpt.id : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Enhanced RAM matching
|
||||||
|
else 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;
|
selectedValId = urlId;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 3. FALLBACK: Stale ID Map
|
// Final fallback - try stale ID map
|
||||||
if (!selectedValId && urlId) {
|
if (!selectedValId && urlId && idToTextMap[urlId]) {
|
||||||
const originalText = idToTextMap[urlId];
|
const originalText = idToTextMap[urlId];
|
||||||
if (originalText) {
|
|
||||||
const cleanTarget = originalText.replace(/\s?[€$£].*/, '').trim().toLowerCase();
|
const cleanTarget = originalText.replace(/\s?[€$£].*/, '').trim().toLowerCase();
|
||||||
const match = opt.values.find(v => v.text.toLowerCase().includes(cleanTarget));
|
const match = opt.values.find(v =>
|
||||||
|
v.text.toLowerCase().includes(cleanTarget) ||
|
||||||
|
cleanTarget.includes(v.text.toLowerCase())
|
||||||
|
);
|
||||||
if (match) selectedValId = match.id;
|
if (match) selectedValId = match.id;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 4. FALLBACK: Generic Text Match (Non-Storage)
|
// Last resort - pick first option (avoid "None" if possible)
|
||||||
if (!selectedValId && opt.type !== 'storage') {
|
|
||||||
if (opt.type === 'ram') {
|
|
||||||
const ramMatch = selectedServer.ram.match(/(\d+)\s*GB/i);
|
|
||||||
const ramVal = ramMatch ? ramMatch[1] : selectedServer.ram.split(' ')[0];
|
|
||||||
const match = opt.values.find(v => v.text.toUpperCase().includes(ramVal.toUpperCase()));
|
|
||||||
if(match) selectedValId = match.id;
|
|
||||||
} else if (opt.type === 'network') {
|
|
||||||
const match = opt.values.find(v => v.text.toLowerCase().includes(selectedServer.network.toLowerCase()));
|
|
||||||
if(match) selectedValId = match.id;
|
|
||||||
} else if (opt.type === 'location') {
|
|
||||||
const match = opt.values.find(v => v.text.includes(selectedServer.location));
|
|
||||||
if(match) selectedValId = match.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. FALLBACK: Default (Prefer None)
|
|
||||||
if (!selectedValId && opt.values.length > 0) {
|
if (!selectedValId && opt.values.length > 0) {
|
||||||
if (opt.type === 'storage') {
|
const nonNoneOptions = opt.values.filter(v =>
|
||||||
const noneOpt = opt.values.find(v => v.text.trim() === '-' || v.text.toLowerCase().includes('none') || v.text.toLowerCase().includes('no hard drive'));
|
v.text.trim() !== '-' &&
|
||||||
selectedValId = noneOpt ? noneOpt.id : opt.values[0].id;
|
!v.text.toLowerCase().includes('none') &&
|
||||||
} else {
|
!v.text.toLowerCase().includes('no hard drive')
|
||||||
selectedValId = opt.values[0].id;
|
);
|
||||||
}
|
selectedValId = nonNoneOptions.length > 0 ? nonNoneOptions[0].id : opt.values[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply selection and price adjustment
|
||||||
if (selectedValId) {
|
if (selectedValId) {
|
||||||
preselected[opt.id] = selectedValId;
|
preselected[opt.id] = selectedValId;
|
||||||
const selectedVal = opt.values.find(v => v.id === selectedValId);
|
const selectedVal = opt.values.find(v => v.id === selectedValId);
|
||||||
@@ -4777,7 +4871,29 @@ var wpcf7 = {
|
|||||||
// Use a unique key for each group to prevent conflicts
|
// Use a unique key for each group to prevent conflicts
|
||||||
const poolKey = (isInstant ? 'instant' : 'custom') + '-g' + groupIndex;
|
const poolKey = (isInstant ? 'instant' : 'custom') + '-g' + groupIndex;
|
||||||
|
|
||||||
// Map slots to pre-selected values if any
|
// For instant customization, try to match storage requirements from server specs
|
||||||
|
let storageRequirements = [];
|
||||||
|
if (isInstant && selectedServer && selectedServer.storage) {
|
||||||
|
const parts = selectedServer.storage.split('+');
|
||||||
|
parts.forEach(part => {
|
||||||
|
const trimmed = part.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
const match = trimmed.match(/^(\d+)x\s+(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
storageRequirements.push({
|
||||||
|
qty: parseInt(match[1]),
|
||||||
|
spec: match[2].trim().toLowerCase()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
storageRequirements.push({
|
||||||
|
qty: 1,
|
||||||
|
spec: trimmed.toLowerCase()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize slots with preselected values or defaults
|
||||||
const initializedSlots = storageOptions.map(opt => {
|
const initializedSlots = storageOptions.map(opt => {
|
||||||
const preId = preselected[opt.id];
|
const preId = preselected[opt.id];
|
||||||
let initialVal = opt.values[0]; // Default
|
let initialVal = opt.values[0]; // Default
|
||||||
@@ -4798,11 +4914,62 @@ var wpcf7 = {
|
|||||||
|
|
||||||
window.pooledState[poolKey] = { slots: initializedSlots };
|
window.pooledState[poolKey] = { slots: initializedSlots };
|
||||||
|
|
||||||
// Pre-set configState
|
// For instant customization, pre-fill with the server's storage configuration
|
||||||
|
if (isInstant && storageRequirements.length > 0) {
|
||||||
|
// 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 matching drive type
|
||||||
|
const match = emptySlot.values.find(v => {
|
||||||
|
const vText = v.text.toLowerCase();
|
||||||
|
// Extract capacity from value
|
||||||
|
const valCapacityMatch = vText.match(/(\d+)\s*tb/i);
|
||||||
|
const valCapacity = valCapacityMatch ? parseInt(valCapacityMatch[1]) : 0;
|
||||||
|
// Extract capacity from requirement
|
||||||
|
const reqCapacityMatch = req.spec.match(/(\d+)\s*tb/i);
|
||||||
|
const reqCapacity = reqCapacityMatch ? parseInt(reqCapacityMatch[1]) : 0;
|
||||||
|
|
||||||
|
// Match capacity and keywords
|
||||||
|
if (valCapacity === reqCapacity && reqCapacity > 0) {
|
||||||
|
// Check for type keywords
|
||||||
|
const keywords = ['nvme', 'ssd', 'gen4', 'gen5', 'enterprise', 'consumer'];
|
||||||
|
const hasKeyword = keywords.some(k =>
|
||||||
|
vText.includes(k) && req.spec.includes(k)
|
||||||
|
);
|
||||||
|
// Check for brand
|
||||||
|
const brands = ['samsung', 'crucial', 'wd', 'seagate', 'kingston'];
|
||||||
|
const hasBrand = brands.some(b =>
|
||||||
|
vText.includes(b) && req.spec.includes(b)
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasKeyword || hasBrand || vText.includes(req.spec);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
emptySlot.currentVal = match;
|
||||||
|
configState[emptySlot.label] = match.price;
|
||||||
|
configIds[emptySlot.id] = match.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Pre-set configState for non-instant or fallback
|
||||||
initializedSlots.forEach(slot => {
|
initializedSlots.forEach(slot => {
|
||||||
configState[slot.label] = slot.currentVal.price;
|
configState[slot.label] = slot.currentVal.price;
|
||||||
configIds[slot.id] = slot.currentVal.id;
|
configIds[slot.id] = slot.currentVal.id;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
templateValues.forEach(val => {
|
templateValues.forEach(val => {
|
||||||
if (val.text.toLowerCase().includes('none') || val.text.toLowerCase().includes('no hard drive')) return;
|
if (val.text.toLowerCase().includes('none') || val.text.toLowerCase().includes('no hard drive')) return;
|
||||||
@@ -4826,7 +4993,24 @@ var wpcf7 = {
|
|||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => updatePooledVisuals(poolKey, isInstant), 100);
|
setTimeout(() => {
|
||||||
|
// Update quantity displays based on current selection
|
||||||
|
initializedSlots.forEach(slot => {
|
||||||
|
if (slot.currentVal && slot.currentVal.text !== '-' &&
|
||||||
|
!slot.currentVal.text.toLowerCase().includes('none') &&
|
||||||
|
!slot.currentVal.text.toLowerCase().includes('no hard drive')) {
|
||||||
|
|
||||||
|
// Increment counter for this value type
|
||||||
|
const qtyEl = document.getElementById(`pool-qty-${poolKey}-${slot.currentVal.text.replace(/\s/g, '')}`);
|
||||||
|
if (qtyEl) {
|
||||||
|
const currentQty = parseInt(qtyEl.textContent) || 0;
|
||||||
|
qtyEl.textContent = currentQty + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updatePooledVisuals(poolKey, isInstant);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user