$(document).ready(function () { // Initialize dropdown functionality function initializeDropdown() { // Toggle dropdown on click $(document).on("click", ".dropdown-selected", function (e) { e.stopPropagation(); const $dropdown = $(this).closest(".custom-dropdown"); const $options = $dropdown.find(".dropdown-options"); const $selected = $(this); // Close other dropdowns $(".custom-dropdown") .not($dropdown) .find(".dropdown-options") .removeClass("show"); $(".custom-dropdown") .not($dropdown) .find(".dropdown-selected") .removeClass("active"); // Toggle current dropdown $options.toggleClass("show"); $selected.toggleClass("active"); }); // Handle option selection $(document).on("click", ".dropdown-option", function (e) { e.stopPropagation(); const $option = $(this); // Ignore disabled options if ($option.hasClass('disabled')) { return; } const $dropdown = $option.closest('.custom-dropdown'); const $selected = $dropdown.find('.dropdown-selected'); const $options = $dropdown.find('.dropdown-options'); const $selectedText = $selected.find('.selected-text'); // Get data from selected option const value = $option.data('value'); const months = $option.data('months'); const price = $option.data('price'); const cycle = $option.data('cycle'); // Get billing cycle // Get text from the first span element to avoid getting badge text const text = $option.find('span').first().text(); // Update selected text $selectedText.text(text); // Update hidden inputs const $form = $dropdown.closest('form'); $form.find('.hiddenMonths').val(months); $form.find('.hiddenProductPrice').val(price); // Update product price $form.find('.hiddenBillingCycle').val(cycle); // Update billing cycle // Update selected state $options.find('.dropdown-option').removeClass('selected'); $option.addClass('selected'); // Close dropdown $options.removeClass('show'); $selected.removeClass('active'); // Trigger custom event for other components $dropdown.trigger('dropdown:changed', { value: value, months: months, price: price, text: text, }); // Update price display using jQuery const $productContainer = $dropdown.closest('[data-product-id]'); const basePrice = parseFloat($productContainer.data('base-price')); const selectedMonths = parseInt(months); // Calculate prices const originalPrice = basePrice * selectedMonths; const savings = selectedMonths >= 12 ? Math.round(originalPrice * 0.1) : 0; const finalPrice = originalPrice - savings; // Update display $productContainer.find('.total-price-display').text('₫' + finalPrice.toLocaleString()); $productContainer.find('.months-display').text(selectedMonths + ' tháng'); $productContainer.find('.price-per-month').text('₫' + basePrice.toLocaleString() + '/tháng'); $productContainer.find('.original-price').text('₫' + originalPrice.toLocaleString()); $productContainer.find('.savings-amount').text('₫' + savings.toLocaleString()); // Show/hide savings info if (savings > 0) { $productContainer.find('.savings-info').show(); } else { $productContainer.find('.savings-info').hide(); } }); // Close dropdown when clicking outside $(document).on("click", function (e) { if (!$(e.target).closest(".custom-dropdown").length) { $(".dropdown-options").removeClass("show"); $(".dropdown-selected").removeClass("active"); } }); // Close dropdown on escape key $(document).on("keydown", function (e) { if (e.key === "Escape") { $(".dropdown-options").removeClass("show"); $(".dropdown-selected").removeClass("active"); } }); } // Initialize on page load initializeDropdown(); // Optimize form submission to prevent blocking $(document).on('submit', 'form[action*="them-gio-hang"]', function(e) { const $form = $(this); const $submitBtn = $form.find('button[type="submit"]'); // Disable submit button to prevent double submission $submitBtn.prop('disabled', true).text('Đang xử lý...'); // Add loading state $form.addClass('submitting'); // Re-enable button after 5 seconds as fallback setTimeout(() => { $submitBtn.prop('disabled', false).text('Đăng ký ngay'); $form.removeClass('submitting'); }, 5000); }); // Cache utility functions - Single JSON approach const VM_INFO_CACHE_KEY = "vm_info_cache"; function saveVMInfoToCache(templateId, data, ttl = 30 * 60 * 1000) { try { const now = new Date().getTime(); let vmInfoCache = getVMInfoCache(); if (!vmInfoCache) { vmInfoCache = { lastUpdated: now, data: {}, }; } vmInfoCache.data[templateId] = { value: data, expiry: now + ttl, }; vmInfoCache.lastUpdated = now; localStorage.setItem( VM_INFO_CACHE_KEY, JSON.stringify(vmInfoCache) ); } catch (e) { console.warn("VM Info cache save failed:", e); } } function getVMInfoFromCache(templateId) { try { const vmInfoCache = getVMInfoCache(); if (!vmInfoCache || !vmInfoCache.data[templateId]) { return null; } const item = vmInfoCache.data[templateId]; const now = new Date().getTime(); // Check if expired if (now > item.expiry) { removeVMInfoFromCache(templateId); return null; } return item.value; } catch (e) { console.warn("VM Info cache get failed:", e); return null; } } function getVMInfoCache() { try { const cacheStr = localStorage.getItem(VM_INFO_CACHE_KEY); return cacheStr ? JSON.parse(cacheStr) : null; } catch (e) { console.warn("VM Info cache parse failed:", e); return null; } } function removeVMInfoFromCache(templateId) { try { const vmInfoCache = getVMInfoCache(); if (vmInfoCache && vmInfoCache.data[templateId]) { delete vmInfoCache.data[templateId]; vmInfoCache.lastUpdated = new Date().getTime(); localStorage.setItem( VM_INFO_CACHE_KEY, JSON.stringify(vmInfoCache) ); } } catch (e) { console.warn("VM Info cache remove failed:", e); } } function clearExpiredVMInfoCache() { try { const vmInfoCache = getVMInfoCache(); if (!vmInfoCache) { return; } const now = new Date().getTime(); let hasExpired = false; Object.keys(vmInfoCache.data).forEach((templateId) => { const item = vmInfoCache.data[templateId]; if (now > item.expiry) { delete vmInfoCache.data[templateId]; hasExpired = true; } }); if (hasExpired) { vmInfoCache.lastUpdated = now; localStorage.setItem( VM_INFO_CACHE_KEY, JSON.stringify(vmInfoCache) ); } } catch (e) { console.warn("VM Info cache cleanup failed:", e); } } // Re-initialize if content is dynamically loaded $(document).on("DOMContentLoaded", initializeDropdown); // Load VM information for each product with caching (OS, CPU, RAM, Disk) - NON-BLOCKING function loadGuestOSInfo() { // Get unique template IDs to avoid duplicate API calls const templateIds = []; $(".guest-os").each(function () { const templateId = $(this).data("template-id"); if (templateId && !templateIds.includes(templateId)) { templateIds.push(templateId); } }); // Load VM info in background without blocking form submission templateIds.forEach((templateId) => { // Check cache first const cachedData = getVMInfoFromCache(templateId); if (cachedData) { // Use cached data updateVMInfoUI(templateId, cachedData); return; } // Add loading indicators addLoadingIndicators(templateId); // Call API to get complete VM info - with shorter timeout $.ajax({ url: `/api/v1/get-vm-info/${templateId}/guest_os,cpu_count,memory_size,disk_capacity`, method: "GET", timeout: 5000, // Reduced timeout to 5 seconds headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr( "content" ), Accept: "application/json", }, success: function (response) { if (response.error === false && response.data) { const vmData = response.data; const processedData = processVMData(vmData); // Cache the result for 30 minutes saveVMInfoToCache(templateId, processedData); // Update UI updateVMInfoUI(templateId, processedData); } else { setFallbackValues(templateId); } }, error: function (xhr, status, error) { console.warn( "Failed to load VM info for template:", templateId, error ); setFallbackValues(templateId); } }); }); } // Load VM info after a short delay to not block page load setTimeout(loadGuestOSInfo, 1000); // Process VM data from API response function processVMData(vmData) { const guestOS = vmData.guest_os || "UNKNOWN"; const cpuCount = vmData.cpu_count || 0; const memoryMB = vmData.memory_size || 0; const diskBytes = vmData.disk_capacity || 0; return { guestOS: guestOS, formattedOS: formatOSName(guestOS), cpu: cpuCount, formattedCPU: `${cpuCount} vCore${cpuCount > 1 ? "s" : ""}`, ram: Math.round((memoryMB / 1024) * 100) / 100, // Convert MB to GB, 2 decimal places formattedRAM: `${Math.round((memoryMB / 1024) * 100) / 100} GB`, disk: Math.round((diskBytes / (1024 * 1024 * 1024)) * 100) / 100, // Convert bytes to GB formattedDisk: `${ Math.round((diskBytes / (1024 * 1024 * 1024)) * 100) / 100 } GB`, }; } // Add loading indicators to all VM info elements function addLoadingIndicators(templateId) { const loadingIcon = 'Đang tải...'; $(`.guest-os[data-template-id="${templateId}"]`).html(loadingIcon); $(`.vm-cpu[data-template-id="${templateId}"]`).html(loadingIcon); $(`.vm-ram[data-template-id="${templateId}"]`).html(loadingIcon); $(`.vm-disk[data-template-id="${templateId}"]`).html(loadingIcon); } // Update all VM info UI elements function updateVMInfoUI(templateId, data) { // Update OS $(`.guest-os[data-template-id="${templateId}"]`).html(data.formattedOS); updateOSIcon(templateId, data.guestOS); // Update hardware specs $(`.vm-cpu[data-template-id="${templateId}"]`).html(data.formattedCPU); $(`.vm-ram[data-template-id="${templateId}"]`).html(data.formattedRAM); $(`.vm-disk[data-template-id="${templateId}"]`).html( data.formattedDisk ); } // Set fallback values when API fails function setFallbackValues(templateId) { $(`.guest-os[data-template-id="${templateId}"]`).html("Linux"); $(`.vm-cpu[data-template-id="${templateId}"]`).html("N/A"); $(`.vm-ram[data-template-id="${templateId}"]`).html("N/A"); $(`.vm-disk[data-template-id="${templateId}"]`).html("N/A"); } // Update OS icon based on detected OS function updateOSIcon(templateId, osName) { const $iconElement = $(`.os-icon[data-template-id="${templateId}"]`); if (osName && osName.toLowerCase().includes("windows")) { $iconElement.removeClass("fab fa-linux").addClass("fab fa-windows"); } else if (osName && osName.toLowerCase().includes("ubuntu")) { $iconElement.removeClass("fab fa-linux").addClass("fab fa-ubuntu"); } else if (osName && osName.toLowerCase().includes("centos")) { $iconElement.removeClass("fab fa-linux").addClass("fab fa-centos"); } else if (osName && osName.toLowerCase().includes("debian")) { $iconElement.removeClass("fab fa-linux").addClass("fab fa-debian"); } else { // Keep linux icon as default $iconElement.removeClass().addClass("fab fa-linux text-orange-600"); } } // Format OS name for better display function formatOSName(osName) { const osMap = { UBUNTU_64: "Ubuntu 64-bit", CENTOS_64: "CentOS 64-bit", CENTOS_7_64: "CentOS 7 64-bit", CENTOS_8_64: "CentOS 8 64-bit", ROCKY_LINUX_64: "Rocky Linux 64-bit", WINDOWS_10_64: "Windows 10 64-bit", WINDOWS_9_SERVER_64: "Windows Server 2019", WINDOWS_SERVER_2019: "Windows Server 2019", WINDOWS_SERVER_2022: "Windows Server 2022", WINDOWS_2019: "Windows Server 2019", WINDOWS_2022: "Windows Server 2022", DEBIAN_10_64: "Debian 10 64-bit", DEBIAN_11_64: "Debian 11 64-bit", OTHER_LINUX_64: "Linux 64-bit", OTHER_64: "Other 64-bit", }; return osMap[osName] || osName || "Linux"; } // Clean up expired cache entries clearExpiredVMInfoCache(); // Load Guest OS info when page loads loadGuestOSInfo(); // Expose cache management to global scope for debugging window.vmInfoCache = { clear: function () { localStorage.removeItem(VM_INFO_CACHE_KEY); }, refresh: function () { this.clear(); loadGuestOSInfo(); }, info: function () { const vmInfoCache = getVMInfoCache(); if (!vmInfoCache) { return; } const templateIds = Object.keys(vmInfoCache.data); templateIds.forEach((templateId) => { const data = getVMInfoFromCache(templateId); }); }, get: function (templateId) { return getVMInfoFromCache(templateId); }, remove: function (templateId) { removeVMInfoFromCache(templateId); }, raw: function () { return getVMInfoCache(); }, }; });