Update baremetal HTML page
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3444,6 +3444,7 @@ var wpcf7 = {
|
|||||||
let storageSelection = {}; // Track individual drive quantities
|
let storageSelection = {}; // Track individual drive quantities
|
||||||
let isInstantCustomized = false; // Track customization state for instant servers
|
let isInstantCustomized = false; // Track customization state for instant servers
|
||||||
let verifiedDefaults = {};
|
let verifiedDefaults = {};
|
||||||
|
let instantSummaryLocked = false;
|
||||||
|
|
||||||
// --- Static Fallback Data (Generated from Live Site) ---
|
// --- Static Fallback Data (Generated from Live Site) ---
|
||||||
const FALLBACK_CONFIG_OPTIONS = [
|
const FALLBACK_CONFIG_OPTIONS = [
|
||||||
@@ -4166,6 +4167,7 @@ var wpcf7 = {
|
|||||||
|
|
||||||
// Reset Customization State
|
// Reset Customization State
|
||||||
isInstantCustomized = false;
|
isInstantCustomized = false;
|
||||||
|
instantSummaryLocked = false;
|
||||||
document.getElementById('instantOptions').style.display = 'none';
|
document.getElementById('instantOptions').style.display = 'none';
|
||||||
const instantContainer = document.getElementById('instantDynamicConfigContainer');
|
const instantContainer = document.getElementById('instantDynamicConfigContainer');
|
||||||
if (instantContainer) instantContainer.innerHTML = '';
|
if (instantContainer) instantContainer.innerHTML = '';
|
||||||
@@ -4177,6 +4179,7 @@ var wpcf7 = {
|
|||||||
|
|
||||||
// Close Instant Customization
|
// Close Instant Customization
|
||||||
function closeInstantCustomization() {
|
function closeInstantCustomization() {
|
||||||
|
instantSummaryLocked = false;
|
||||||
isInstantCustomized = false;
|
isInstantCustomized = false;
|
||||||
document.getElementById('instantOptions').style.display = 'none';
|
document.getElementById('instantOptions').style.display = 'none';
|
||||||
document.getElementById('instantServersGrid').style.display = 'grid';
|
document.getElementById('instantServersGrid').style.display = 'grid';
|
||||||
@@ -4207,6 +4210,8 @@ var wpcf7 = {
|
|||||||
document.getElementById('btnCustomizeInstant').addEventListener('click', async function(e) {
|
document.getElementById('btnCustomizeInstant').addEventListener('click', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
isInstantCustomized = true;
|
isInstantCustomized = true;
|
||||||
|
instantSummaryLocked = true;
|
||||||
|
applyBaseStorageSummary();
|
||||||
|
|
||||||
const instantOptions = document.getElementById('instantOptions');
|
const instantOptions = document.getElementById('instantOptions');
|
||||||
const serversGrid = document.getElementById('instantServersGrid');
|
const serversGrid = document.getElementById('instantServersGrid');
|
||||||
@@ -4259,83 +4264,18 @@ var wpcf7 = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// For instant customization, we need to map the actual storage requirements to available options
|
// For instant customization, prioritize the preselected IDs from URL
|
||||||
// Parse storage requirements from description and match them to available options
|
// These are the exact configurations from WHMCS
|
||||||
console.log("Processing instant server with storage:", selectedServer.storage);
|
|
||||||
|
|
||||||
// Parse storage requirements from description
|
|
||||||
const storageReqs = [];
|
|
||||||
const parts = selectedServer.storage.split('+');
|
|
||||||
parts.forEach(part => {
|
|
||||||
const trimmed = part.trim();
|
|
||||||
if (!trimmed) return;
|
|
||||||
const match = trimmed.match(/^(\d+)x\s+(.+)$/);
|
|
||||||
if (match) {
|
|
||||||
storageReqs.push({
|
|
||||||
qty: parseInt(match[1]),
|
|
||||||
spec: match[2].trim()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Map requirements to available storage options
|
|
||||||
let reqIndex = 0;
|
|
||||||
|
|
||||||
options.forEach(opt => {
|
options.forEach(opt => {
|
||||||
let selectedValId = null;
|
let selectedValId = preselected[opt.id]; // Use the ID directly from WHMCS
|
||||||
|
|
||||||
if (opt.type === 'storage' && reqIndex < storageReqs.length) {
|
// Validate that the preselected ID exists in the options
|
||||||
const req = storageReqs[reqIndex];
|
if (selectedValId && !opt.values.find(v => v.id === selectedValId)) {
|
||||||
let bestMatch = null;
|
console.warn(`Invalid preselected ID ${selectedValId} for option ${opt.id}`);
|
||||||
let bestScore = 0;
|
selectedValId = null;
|
||||||
|
|
||||||
// Find the best matching option for this requirement
|
|
||||||
opt.values.forEach(val => {
|
|
||||||
let score = 0;
|
|
||||||
const valText = val.text.toLowerCase();
|
|
||||||
const reqSpec = req.spec.toLowerCase();
|
|
||||||
|
|
||||||
// Check capacity match (exact match required)
|
|
||||||
const valCap = valText.match(/(\d+(?:\.\d+)?)\s*tb/i);
|
|
||||||
const reqCap = reqSpec.match(/(\d+(?:\.\d+)?)\s*tb/i);
|
|
||||||
if (valCap && reqCap && valCap[1] === reqCap[1]) {
|
|
||||||
score += 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check brand match
|
|
||||||
if (valText.includes('crucial') && reqSpec.includes('crucial')) score += 50;
|
|
||||||
if (valText.includes('kioxia') && reqSpec.includes('kioxia')) score += 50;
|
|
||||||
if (valText.includes('samsung') && reqSpec.includes('samsung')) score += 50;
|
|
||||||
|
|
||||||
// Check model match
|
|
||||||
if (valText.includes('t705') && reqSpec.includes('t705')) score += 50;
|
|
||||||
if (valText.includes('cm7') && reqSpec.includes('cm7')) score += 50;
|
|
||||||
|
|
||||||
if (score > bestScore) {
|
|
||||||
bestScore = score;
|
|
||||||
bestMatch = val;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bestMatch && bestScore >= 100) {
|
|
||||||
selectedValId = bestMatch.id;
|
|
||||||
preselected[opt.id] = selectedValId;
|
|
||||||
reqIndex++;
|
|
||||||
} else {
|
|
||||||
// If no match found, try to use URL preselected value
|
|
||||||
if (preselected[opt.id]) {
|
|
||||||
const urlMatch = opt.values.find(v => v.id === preselected[opt.id]);
|
|
||||||
if (urlMatch) {
|
|
||||||
selectedValId = preselected[opt.id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (opt.type !== 'storage' && preselected[opt.id]) {
|
|
||||||
// Use preselected IDs for non-storage options
|
|
||||||
selectedValId = preselected[opt.id];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply price adjustment for selected non-storage options
|
// For non-storage options, apply price adjustment using preselected IDs
|
||||||
if (opt.type !== 'storage' && selectedValId) {
|
if (opt.type !== 'storage' && selectedValId) {
|
||||||
const selectedVal = opt.values.find(v => v.id === selectedValId);
|
const selectedVal = opt.values.find(v => v.id === selectedValId);
|
||||||
if (selectedVal && selectedVal.price > 0) {
|
if (selectedVal && selectedVal.price > 0) {
|
||||||
@@ -4348,8 +4288,7 @@ var wpcf7 = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Render options with the exact WHMCS preselected values
|
// Render options with the exact WHMCS preselected values
|
||||||
// Also pass the storage requirements for proper pre-filling
|
renderDynamicOptions(options, true, preselected);
|
||||||
renderDynamicOptions(options, true, preselected, storageReqs);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
renderFallbackOptions(container);
|
renderFallbackOptions(container);
|
||||||
@@ -4704,10 +4643,13 @@ var wpcf7 = {
|
|||||||
updateSummary();
|
updateSummary();
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderDynamicOptions(options, isInstant = false, preselected = {}, storageRequirements = []) {
|
function renderDynamicOptions(options, isInstant = false, preselected = {}) {
|
||||||
const container = isInstant ? document.getElementById('instantDynamicConfigContainer') : document.getElementById('dynamicConfigContainer');
|
const container = isInstant ? document.getElementById('instantDynamicConfigContainer') : document.getElementById('dynamicConfigContainer');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
const planStorageCounts = isInstant ? parsePlanStorageCounts(selectedServer?.storage) : {};
|
||||||
|
const planTargets = isInstant ? buildPlanTargets(selectedServer?.storage) : [];
|
||||||
|
|
||||||
const groups = { core: [], storage: [], network: [], other: [] };
|
const groups = { core: [], storage: [], network: [], other: [] };
|
||||||
|
|
||||||
options.forEach(opt => {
|
options.forEach(opt => {
|
||||||
@@ -4804,7 +4746,7 @@ var wpcf7 = {
|
|||||||
container.appendChild(visualContainer);
|
container.appendChild(visualContainer);
|
||||||
|
|
||||||
// Always use pooled grid for these groups as they are by definition identical
|
// 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, planStorageCounts, groupIndex, planTargets));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4831,7 +4773,206 @@ var wpcf7 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New Pooled Storage Grid
|
// New Pooled Storage Grid
|
||||||
function createPooledStorageGrid(storageOptions, isInstant, preselected = {}, groupIndex = 0, storageRequirements = []) {
|
function normalizeStorageName(name) {
|
||||||
|
return String(name || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[\s\u00A0]+/g, ' ')
|
||||||
|
.replace(/€|eur|ƒ|ª|[,/\\]/g, ' ')
|
||||||
|
.replace(/[^a-z0-9 ]/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeStorageLabel(name) {
|
||||||
|
const stopWords = new Set(['eur', 'nvme']);
|
||||||
|
return normalizeStorageName(name)
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(token => token && !stopWords.has(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePlanStorageCounts(raw) {
|
||||||
|
const counts = {};
|
||||||
|
if (!raw) return counts;
|
||||||
|
|
||||||
|
const parts = raw.split('+').map(part => part.trim()).filter(Boolean);
|
||||||
|
parts.forEach(part => {
|
||||||
|
const match = part.match(/(\d+)\s*x?\s*(.+)/i);
|
||||||
|
const qty = match ? parseInt(match[1], 10) : 1;
|
||||||
|
const label = match ? match[2].trim() : part;
|
||||||
|
const tokens = tokenizeStorageLabel(label);
|
||||||
|
if (!tokens.length) return;
|
||||||
|
const key = tokens.join(' ');
|
||||||
|
|
||||||
|
if (!counts[key]) {
|
||||||
|
counts[key] = { count: 0, tokens };
|
||||||
|
}
|
||||||
|
counts[key].count += qty > 0 ? qty : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlanTargets(raw) {
|
||||||
|
const targets = [];
|
||||||
|
if (!raw) return targets;
|
||||||
|
|
||||||
|
const parts = raw.split('+').map(part => part.trim()).filter(Boolean);
|
||||||
|
parts.forEach(part => {
|
||||||
|
const match = part.match(/(\d+)\s*x?\s*(.+)/i);
|
||||||
|
const qty = match ? parseInt(match[1], 10) : 1;
|
||||||
|
const label = match ? match[2].trim() : part;
|
||||||
|
const normalized = normalizeStorageName(label);
|
||||||
|
if (!normalized) return;
|
||||||
|
for (let i = 0; i < (qty > 0 ? qty : 1); i++) {
|
||||||
|
targets.push({ normalized, label });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrementCount(counts, key) {
|
||||||
|
const entry = counts[key];
|
||||||
|
if (!entry || entry.count <= 0) return false;
|
||||||
|
entry.count--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumePlanStorageCount(counts, text) {
|
||||||
|
const normalized = normalizeStorageName(text);
|
||||||
|
if (!normalized) return false;
|
||||||
|
if (counts[normalized] && counts[normalized].count > 0) {
|
||||||
|
return decrementCount(counts, normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(counts)) {
|
||||||
|
const entry = counts[key];
|
||||||
|
if (entry.count <= 0) continue;
|
||||||
|
if (key.includes(normalized) || normalized.includes(key)) {
|
||||||
|
entry.count--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldAssignPlanValue(slot, value, counts) {
|
||||||
|
const valueTokens = tokenizeStorageLabel(value.text);
|
||||||
|
for (const key of Object.keys(counts)) {
|
||||||
|
const entry = counts[key];
|
||||||
|
if (entry.count <= 0) continue;
|
||||||
|
|
||||||
|
const hasIntersection = entry.tokens.every(token => valueTokens.includes(token)) ||
|
||||||
|
valueTokens.every(token => entry.tokens.includes(token));
|
||||||
|
|
||||||
|
if (hasIntersection) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignInstantSlotValue(slot, value, preselected) {
|
||||||
|
slot.currentVal = value;
|
||||||
|
configState[slot.label] = 0;
|
||||||
|
configIds[slot.id] = value.id;
|
||||||
|
const displayName = value.text.replace(/\s?\(.*?\)/, '');
|
||||||
|
storageSelection[slot.id] = { name: displayName, price: 0 };
|
||||||
|
|
||||||
|
if (window.originalDrives) {
|
||||||
|
window.originalDrives.push({
|
||||||
|
slotId: slot.id,
|
||||||
|
driveId: value.id,
|
||||||
|
text: value.text,
|
||||||
|
price: value.price
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preselected) {
|
||||||
|
preselected[slot.id] = value.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPlanTargets(initializedSlots, targets, preselected) {
|
||||||
|
let index = 0;
|
||||||
|
while (index < targets.length) {
|
||||||
|
const target = targets[index];
|
||||||
|
let matchedSlot = null;
|
||||||
|
|
||||||
|
for (const slot of initializedSlots) {
|
||||||
|
if (slot.currentVal !== slot.values[0]) continue;
|
||||||
|
|
||||||
|
const matched = slot.values.find(value => {
|
||||||
|
const valueNorm = normalizeStorageName(value.text);
|
||||||
|
const targetNorm = target.normalized;
|
||||||
|
return valueNorm.includes(targetNorm) || targetNorm.includes(valueNorm);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
matchedSlot = { slot, value: matched };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedSlot) {
|
||||||
|
assignInstantSlotValue(matchedSlot.slot, matchedSlot.value, preselected);
|
||||||
|
targets.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.length === 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPlanStorageFallback(initializedSlots, counts, preselected) {
|
||||||
|
initializedSlots.forEach(slot => {
|
||||||
|
if (slot.currentVal !== slot.values[0]) return;
|
||||||
|
|
||||||
|
const matchKey = slot.values.reduce((matched, val) => {
|
||||||
|
if (matched) return matched;
|
||||||
|
const hitsKey = shouldAssignPlanValue(slot, val, counts);
|
||||||
|
return hitsKey ? { key: hitsKey, val } : null;
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
if (matchKey && matchKey.val) {
|
||||||
|
slot.currentVal = matchKey.val;
|
||||||
|
consumePlanStorageCount(counts, matchKey.val.text);
|
||||||
|
|
||||||
|
assignInstantSlotValue(slot, matchKey.val, preselected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyStorageSelections(poolKey, counts, preselected) {
|
||||||
|
if (!counts || Object.keys(counts).length === 0) return;
|
||||||
|
const pool = window.pooledState[poolKey];
|
||||||
|
if (!pool) return;
|
||||||
|
|
||||||
|
const needs = {};
|
||||||
|
Object.entries(counts).forEach(([key, entry]) => {
|
||||||
|
if (entry.count > 0) {
|
||||||
|
needs[key] = { count: entry.count, tokens: entry.tokens };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!Object.keys(needs).length) return;
|
||||||
|
|
||||||
|
pool.slots.forEach(slot => {
|
||||||
|
if (slot.currentVal !== slot.values[0]) return;
|
||||||
|
|
||||||
|
const matchKey = slot.values.reduce((matched, val) => {
|
||||||
|
if (matched) return matched;
|
||||||
|
const hitsKey = shouldAssignPlanValue(slot, val, needs);
|
||||||
|
return hitsKey ? { key: hitsKey, val } : null;
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
if (matchKey && matchKey.val) {
|
||||||
|
assignInstantSlotValue(slot, matchKey.val, preselected);
|
||||||
|
consumePlanStorageCount(needs, matchKey.val.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPooledStorageGrid(storageOptions, isInstant, preselected = {}, planCounts = {}, groupIndex = 0, planTargets = []) {
|
||||||
const grid = document.createElement('div');
|
const grid = document.createElement('div');
|
||||||
grid.className = 'config-grid';
|
grid.className = 'config-grid';
|
||||||
|
|
||||||
@@ -4843,13 +4984,17 @@ var wpcf7 = {
|
|||||||
|
|
||||||
// Initialize slots with preselected values from WHMCS or defaults
|
// Initialize slots with preselected values from WHMCS or defaults
|
||||||
const initializedSlots = storageOptions.map(opt => {
|
const initializedSlots = storageOptions.map(opt => {
|
||||||
const preId = preselected[opt.id];
|
const slotKey = opt.id?.toString ? opt.id.toString() : opt.id;
|
||||||
|
const preId = preselected[slotKey] || preselected[Number(slotKey)];
|
||||||
let initialVal = opt.values[0]; // Default (usually None)
|
let initialVal = opt.values[0]; // Default (usually None)
|
||||||
|
|
||||||
// Use the preselected value from WHMCS if available
|
// Use the preselected value from WHMCS if available
|
||||||
if (preId) {
|
if (preId) {
|
||||||
const found = opt.values.find(v => v.id === preId);
|
const found = opt.values.find(v => `${v.id}` === `${preId}`);
|
||||||
if (found) initialVal = found;
|
if (found) {
|
||||||
|
initialVal = found;
|
||||||
|
consumePlanStorageCount(planCounts, found.text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -4862,90 +5007,65 @@ var wpcf7 = {
|
|||||||
|
|
||||||
window.pooledState[poolKey] = { slots: initializedSlots };
|
window.pooledState[poolKey] = { slots: initializedSlots };
|
||||||
|
|
||||||
// For instant customization, track original drives and pre-fill with requirements
|
// For instant customization, track original drives and use preselected IDs from URL
|
||||||
if (isInstant) {
|
if (isInstant) {
|
||||||
window.originalDrives = [];
|
window.originalDrives = [];
|
||||||
|
storageSelection = {};
|
||||||
|
|
||||||
// First reset all slots to None
|
// Set up the preselected drives from the URL configoption IDs
|
||||||
initializedSlots.forEach(slot => {
|
initializedSlots.forEach(slot => {
|
||||||
const noneOption = slot.values.find(v =>
|
// Use the preselected ID from URL for this slot
|
||||||
v.text.trim() === '-' ||
|
const slotKey = slot.id?.toString ? slot.id.toString() : slot.id;
|
||||||
v.text.toLowerCase().includes('none') ||
|
const preselectedId = preselected[slotKey] || preselected[Number(slotKey)];
|
||||||
v.text.toLowerCase().includes('no hard drive')
|
if (preselectedId) {
|
||||||
);
|
const preselectedValue = slot.values.find(v => `${v.id}` === `${preselectedId}`);
|
||||||
if (noneOption) {
|
if (preselectedValue) {
|
||||||
slot.currentVal = noneOption;
|
// Use the preselected value from WHMCS URL
|
||||||
configState[slot.label] = noneOption.price;
|
slot.currentVal = preselectedValue;
|
||||||
configIds[slot.id] = noneOption.id;
|
configState[slot.label] = 0; // Included in base price
|
||||||
}
|
configIds[slot.id] = preselectedId;
|
||||||
});
|
|
||||||
|
|
||||||
// Fill slots based on storage requirements
|
// Store in storageSelection for summary
|
||||||
if (storageRequirements && storageRequirements.length > 0) {
|
const displayName = preselectedValue.text.replace(/\s?\(.*?\)/, '');
|
||||||
// Clear storageSelection to start fresh
|
storageSelection[slot.id] = {
|
||||||
storageSelection = {};
|
name: displayName,
|
||||||
|
price: 0 // Free for included drives
|
||||||
|
};
|
||||||
|
|
||||||
storageRequirements.forEach(req => {
|
// Track as original drive
|
||||||
for (let i = 0; i < req.qty; i++) {
|
window.originalDrives.push({
|
||||||
// Find an empty slot
|
slotId: slot.id,
|
||||||
const emptySlot = initializedSlots.find(s =>
|
driveId: preselectedId,
|
||||||
s.currentVal.text === '-' ||
|
text: preselectedValue.text,
|
||||||
s.currentVal.text.toLowerCase().includes('none')
|
price: preselectedValue.price
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If preselected ID not found, use default (None)
|
||||||
|
const noneOption = slot.values.find(v =>
|
||||||
|
v.text.trim() === '-' ||
|
||||||
|
v.text.toLowerCase().includes('none') ||
|
||||||
|
v.text.toLowerCase().includes('no hard drive')
|
||||||
);
|
);
|
||||||
|
if (noneOption) {
|
||||||
if (emptySlot) {
|
slot.currentVal = noneOption;
|
||||||
console.log(`Filling slot ${emptySlot.id} with ${req.spec}`);
|
configState[slot.label] = noneOption.price;
|
||||||
|
configIds[slot.id] = noneOption.id;
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Store in storageSelection for summary
|
|
||||||
const displayName = match.text.replace(/\s?\(.*?\)/, '');
|
|
||||||
storageSelection[emptySlot.id] = {
|
|
||||||
name: displayName,
|
|
||||||
price: 0 // Free for included drives
|
|
||||||
};
|
|
||||||
|
|
||||||
// Track as original drive
|
|
||||||
window.originalDrives.push({
|
|
||||||
slotId: emptySlot.id,
|
|
||||||
driveId: match.id,
|
|
||||||
text: match.text,
|
|
||||||
price: match.price
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✓ Filled with: ${match.text}`);
|
|
||||||
} else {
|
|
||||||
console.error(`✗ Could not find matching drive for: ${req.spec}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`✗ No empty slots available for: ${req.spec}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
// If no preselected ID for this slot, use default (None)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Pre-set configState for custom servers
|
// Pre-set configState for custom servers
|
||||||
initializedSlots.forEach(slot => {
|
initializedSlots.forEach(slot => {
|
||||||
@@ -4954,12 +5074,20 @@ var wpcf7 = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isInstant && Object.keys(planCounts).length > 0) {
|
||||||
|
applyPlanStorageFallback(initializedSlots, planCounts, preselected);
|
||||||
|
if (planTargets.length > 0) {
|
||||||
|
applyPlanTargets(initializedSlots, planTargets, preselected);
|
||||||
|
}
|
||||||
|
verifyStorageSelections(poolKey, planCounts, preselected);
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'config-card has-stepper';
|
card.className = 'config-card has-stepper';
|
||||||
card.id = `pool-card-${val.text.replace(/\s/g, '')}`;
|
card.id = `pool-card-${val.id}`;
|
||||||
|
|
||||||
let priceText = val.price === 0 ? 'Included' : `+€${val.price}`;
|
let priceText = val.price === 0 ? 'Included' : `+€${val.price}`;
|
||||||
let displayName = val.text.replace(/\s?\(.*?\)/, '');
|
let displayName = val.text.replace(/\s?\(.*?\)/, '');
|
||||||
@@ -4968,9 +5096,9 @@ var wpcf7 = {
|
|||||||
<div class="option-name">${displayName}</div>
|
<div class="option-name">${displayName}</div>
|
||||||
<div class="option-price">${priceText}</div>
|
<div class="option-price">${priceText}</div>
|
||||||
<div class="qty-stepper">
|
<div class="qty-stepper">
|
||||||
<button class="btn-qty" onclick="updatePooledQty('${poolKey}', '${val.text.replace(/'/g, "\\'")}', -1, ${isInstant})">-</button>
|
<button class="btn-qty" onclick="updatePooledQty('${poolKey}', '${val.id}', -1, ${isInstant})">-</button>
|
||||||
<span class="qty-val" id="pool-qty-${poolKey}-${val.text.replace(/\s/g, '')}">0</span>
|
<span class="qty-val" id="pool-qty-${poolKey}-${val.id}">0</span>
|
||||||
<button class="btn-qty" onclick="updatePooledQty('${poolKey}', '${val.text.replace(/'/g, "\\'")}', 1, ${isInstant})">+</button>
|
<button class="btn-qty" onclick="updatePooledQty('${poolKey}', '${val.id}', 1, ${isInstant})">+</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
@@ -4979,17 +5107,17 @@ var wpcf7 = {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Update quantity displays based on current selection
|
// Update quantity displays based on current selection
|
||||||
initializedSlots.forEach(slot => {
|
initializedSlots.forEach(slot => {
|
||||||
if (slot.currentVal && slot.currentVal.text !== '-' &&
|
if (slot.currentVal && slot.currentVal.text !== '-' &&
|
||||||
!slot.currentVal.text.toLowerCase().includes('none') &&
|
!slot.currentVal.text.toLowerCase().includes('none') &&
|
||||||
!slot.currentVal.text.toLowerCase().includes('no hard drive')) {
|
!slot.currentVal.text.toLowerCase().includes('no hard drive')) {
|
||||||
|
|
||||||
// Increment counter for this value type
|
// Increment counter for this value type
|
||||||
const qtyEl = document.getElementById(`pool-qty-${poolKey}-${slot.currentVal.text.replace(/\s/g, '')}`);
|
const qtyEl = document.getElementById(`pool-qty-${poolKey}-${slot.currentVal.id || slot.currentVal.text}`);
|
||||||
if (qtyEl) {
|
if (qtyEl) {
|
||||||
const currentQty = parseInt(qtyEl.textContent) || 0;
|
const currentQty = parseInt(qtyEl.textContent) || 0;
|
||||||
qtyEl.textContent = currentQty + 1;
|
qtyEl.textContent = currentQty + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
updatePooledVisuals(poolKey, isInstant);
|
updatePooledVisuals(poolKey, isInstant);
|
||||||
@@ -5005,65 +5133,53 @@ var wpcf7 = {
|
|||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.updatePooledQty = function(poolKey, valText, change, isInstant) {
|
window.updatePooledQty = function(poolKey, optionId, change, isInstant) {
|
||||||
|
if (instantSummaryLocked) {
|
||||||
|
instantSummaryLocked = false;
|
||||||
|
}
|
||||||
const pool = window.pooledState[poolKey];
|
const pool = window.pooledState[poolKey];
|
||||||
if (!pool) return;
|
if (!pool) return;
|
||||||
|
|
||||||
if (change > 0) {
|
if (change > 0) {
|
||||||
// Find first slot that is "None" or default
|
// Find first slot that is "None" or default
|
||||||
const targetSlot = pool.slots.find(s => s.currentVal === s.values[0]);
|
const targetSlot = pool.slots.find(s =>
|
||||||
|
s.currentVal.text === '-' ||
|
||||||
|
s.currentVal.text.toLowerCase().includes('none') ||
|
||||||
|
s.currentVal.text.toLowerCase().includes('no hard drive')
|
||||||
|
);
|
||||||
|
|
||||||
if (targetSlot) {
|
if (targetSlot) {
|
||||||
// Find the matching value object in this slot's values
|
// Find the matching value object in this slot's values
|
||||||
const newVal = targetSlot.values.find(v => v.text === valText);
|
const newVal = targetSlot.values.find(v => v.id === optionId);
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
targetSlot.currentVal = newVal;
|
// For instant customization, handle pricing correctly
|
||||||
|
|
||||||
// For instant customization, calculate price difference
|
|
||||||
if (isInstant && window.originalDrives) {
|
if (isInstant && window.originalDrives) {
|
||||||
// Check if this slot had an original drive
|
// For instant customization, the original drives are included in base price
|
||||||
const originalDrive = window.originalDrives.find(d => d.slotId === targetSlot.id);
|
// When adding a new drive, charge the full price of the new drive
|
||||||
if (originalDrive) {
|
configState[targetSlot.label] = newVal.price;
|
||||||
// Calculate price difference
|
|
||||||
const priceDiff = newVal.price - originalDrive.price;
|
|
||||||
configState[targetSlot.label] = priceDiff;
|
|
||||||
|
|
||||||
// Update original drives tracking
|
|
||||||
originalDrive.driveId = newVal.id;
|
|
||||||
originalDrive.text = newVal.text;
|
|
||||||
originalDrive.price = newVal.price;
|
|
||||||
} else {
|
|
||||||
// This is a new drive, charge full price
|
|
||||||
configState[targetSlot.label] = newVal.price;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Custom server or no tracking, charge full price
|
// Custom server or no tracking, charge full price
|
||||||
configState[targetSlot.label] = newVal.price;
|
configState[targetSlot.label] = newVal.price;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetSlot.currentVal = newVal;
|
||||||
configIds[targetSlot.id] = newVal.id;
|
configIds[targetSlot.id] = newVal.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Decrement: Find last slot that matches this value
|
// Decrement: Find last slot that matches this value
|
||||||
for (let i = pool.slots.length - 1; i >= 0; i--) {
|
for (let i = pool.slots.length - 1; i >= 0; i--) {
|
||||||
if (pool.slots[i].currentVal.text === valText) {
|
if (pool.slots[i].currentVal.id === optionId) {
|
||||||
// Reset to default
|
// Reset to default
|
||||||
const defaultVal = pool.slots[i].values[0];
|
const defaultVal = pool.slots[i].values[0];
|
||||||
|
const originalCurrentPrice = pool.slots[i].currentVal.price;
|
||||||
|
|
||||||
pool.slots[i].currentVal = defaultVal;
|
pool.slots[i].currentVal = defaultVal;
|
||||||
|
|
||||||
// For instant customization, refund the price if it was an upgrade
|
// For instant customization, reset to default state
|
||||||
if (isInstant && window.originalDrives) {
|
if (isInstant) {
|
||||||
const originalDrive = window.originalDrives.find(d => d.slotId === pool.slots[i].id);
|
// When removing a drive that was added, reset to 0 (no additional charge)
|
||||||
if (originalDrive && pool.slots[i].currentVal.id !== originalDrive.driveId) {
|
configState[pool.slots[i].label] = 0;
|
||||||
// We're removing an upgrade, refund the difference
|
|
||||||
const currentVal = pool.slots[i].values.find(v => v.id === configIds[pool.slots[i].id]);
|
|
||||||
if (currentVal) {
|
|
||||||
configState[pool.slots[i].label] = -currentVal.price;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
configState[pool.slots[i].label] = defaultVal.price;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
configState[pool.slots[i].label] = defaultVal.price;
|
configState[pool.slots[i].label] = defaultVal.price;
|
||||||
}
|
}
|
||||||
@@ -5108,7 +5224,7 @@ var wpcf7 = {
|
|||||||
filledCount++;
|
filledCount++;
|
||||||
|
|
||||||
// Track quantity for stepper numbers
|
// Track quantity for stepper numbers
|
||||||
const key = slot.currentVal.text.replace(/\s/g, '');
|
const key = slot.currentVal.id || slot.currentVal.text.replace(/\s/g, '');
|
||||||
qtyMap[key] = (qtyMap[key] || 0) + 1;
|
qtyMap[key] = (qtyMap[key] || 0) + 1;
|
||||||
} else {
|
} else {
|
||||||
slots[index].classList.remove('filled');
|
slots[index].classList.remove('filled');
|
||||||
@@ -5165,12 +5281,46 @@ var wpcf7 = {
|
|||||||
if(specStorage) specStorage.innerHTML = pillsHtml;
|
if(specStorage) specStorage.innerHTML = pillsHtml;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function formatStorageSummaryHtml(storageString) {
|
||||||
|
if (!storageString) {
|
||||||
|
return '<span style="color: var(--text-secondary); font-style: italic;">No drives selected</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = storageString.split('+').map(part => part.trim()).filter(Boolean);
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return '<span style="color: var(--text-secondary); font-style: italic;">No drives selected</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.map(part => `
|
||||||
|
<span class="spec-pill" style="margin-right:4px; margin-bottom:4px; display:inline-block;">${part}</span>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBaseStorageSummary() {
|
||||||
|
const summaryStorage = document.getElementById('summaryStorage');
|
||||||
|
const specStorage = document.getElementById('spec-storage-config-hero');
|
||||||
|
const storageDetails = selectedServer?.storage || null;
|
||||||
|
|
||||||
|
if (summaryStorage) {
|
||||||
|
summaryStorage.innerHTML = formatStorageSummaryHtml(storageDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (specStorage) {
|
||||||
|
specStorage.textContent = storageDetails || '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to update storage summary with enhanced styling
|
// Function to update storage summary with enhanced styling
|
||||||
function updateStorageSummary() {
|
function updateStorageSummary() {
|
||||||
// Collect all selected drives
|
// Collect all selected drives
|
||||||
const storageCount = {};
|
const storageCount = {};
|
||||||
let storageHtml = '';
|
let storageHtml = '';
|
||||||
|
|
||||||
|
if (instantSummaryLocked) {
|
||||||
|
applyBaseStorageSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Object.values(storageSelection).forEach(drive => {
|
Object.values(storageSelection).forEach(drive => {
|
||||||
if (drive.name && !drive.name.toLowerCase().includes('none')) {
|
if (drive.name && !drive.name.toLowerCase().includes('none')) {
|
||||||
storageCount[drive.name] = (storageCount[drive.name] || 0) + 1;
|
storageCount[drive.name] = (storageCount[drive.name] || 0) + 1;
|
||||||
@@ -5178,7 +5328,10 @@ var wpcf7 = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(storageCount).length === 0) {
|
if (Object.keys(storageCount).length === 0) {
|
||||||
document.getElementById('summaryStorage').innerHTML = '<span style="color: var(--text-secondary); font-style: italic;">No drives selected</span>';
|
const summaryStorage = document.getElementById('summaryStorage');
|
||||||
|
if (summaryStorage) {
|
||||||
|
summaryStorage.innerHTML = '<span style="color: var(--text-secondary); font-style: italic;">No drives selected</span>';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create styled drive pills
|
// Create styled drive pills
|
||||||
Object.entries(storageCount).forEach(([name, count], index) => {
|
Object.entries(storageCount).forEach(([name, count], index) => {
|
||||||
@@ -5215,11 +5368,16 @@ var wpcf7 = {
|
|||||||
if (specStorage) {
|
if (specStorage) {
|
||||||
const storageText = Object.entries(storageCount)
|
const storageText = Object.entries(storageCount)
|
||||||
.map(([name, count]) => `${count}x ${name}`)
|
.map(([name, count]) => `${count}x ${name}`)
|
||||||
.join(' + ') || 'None selected';
|
.join(' + ');
|
||||||
specStorage.textContent = storageText;
|
specStorage.textContent = storageText || selectedServer?.storage || '-';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePreselectedId(optionId, preselected) {
|
||||||
|
const key = optionId?.toString ? optionId.toString() : optionId;
|
||||||
|
return preselected[key] || preselected[Number(key)];
|
||||||
|
}
|
||||||
|
|
||||||
function createOptionGrid(opt, isWide, storageIndex = -1, isInstant = false, preselected = {}) {
|
function createOptionGrid(opt, isWide, storageIndex = -1, isInstant = false, preselected = {}) {
|
||||||
const grid = document.createElement('div');
|
const grid = document.createElement('div');
|
||||||
grid.className = isWide ? 'config-grid wide' : 'config-grid';
|
grid.className = isWide ? 'config-grid wide' : 'config-grid';
|
||||||
@@ -5229,7 +5387,8 @@ var wpcf7 = {
|
|||||||
card.className = 'config-card';
|
card.className = 'config-card';
|
||||||
|
|
||||||
// Check if this value is the preselected one
|
// Check if this value is the preselected one
|
||||||
const isPreselected = preselected[opt.id] && preselected[opt.id] === val.id;
|
const preId = resolvePreselectedId(opt.id, preselected);
|
||||||
|
const isPreselected = preId && `${preId}` === `${val.id}`;
|
||||||
// Default behavior: Select if preselected, OR if no preselection exists and it's the first item
|
// Default behavior: Select if preselected, OR if no preselection exists and it's the first item
|
||||||
const isActive = isPreselected || (!preselected[opt.id] && index === 0);
|
const isActive = isPreselected || (!preselected[opt.id] && index === 0);
|
||||||
|
|
||||||
@@ -5273,6 +5432,7 @@ var wpcf7 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Always update the storage summary
|
// Always update the storage summary
|
||||||
|
if (instantSummaryLocked) instantSummaryLocked = false;
|
||||||
updateStorageSummary();
|
updateStorageSummary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5677,17 +5837,37 @@ var wpcf7 = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize
|
function initializePage() {
|
||||||
renderInstantServers();
|
try {
|
||||||
renderCustomServers();
|
renderInstantServers();
|
||||||
|
renderCustomServers();
|
||||||
|
|
||||||
// Trigger initial OOS check for all instant servers on page load
|
// Pre-select the first instant server so the UI is active immediately
|
||||||
prefetchStockData();
|
if(instantServers.length > 0) {
|
||||||
fetchInstantPrices();
|
selectInstantServer(instantServers[0].id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during initial rendering:", error);
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-select the first instant server so the UI is active immediately
|
// Trigger background tasks that could potentially fail
|
||||||
if(instantServers.length > 0) {
|
try {
|
||||||
selectInstantServer(instantServers[0].id);
|
prefetchStockData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in prefetchStockData:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fetchInstantPrices();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in fetchInstantPrices:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
|
} else {
|
||||||
|
initializePage();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user