- Added new fields 'billing_cycle' and 'product_pricing_id' to Cart and OrderItem models. - Updated product retrieval logic to support both new and legacy systems. - Introduced relationships for Product and ProductPricings in Cart and OrderItem models. - Created migration to add new fields to the carts and order_item tables. - Updated frontend service index to handle new pricing structure and billing cycles. - Improved UI components for better user experience in product selection and pricing display. - Refactored action buttons in various views for consistency and improved styling.
446 lines
16 KiB
JavaScript
446 lines
16 KiB
JavaScript
$(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 =
|
|
'<i class="fas fa-spinner fa-spin text-orange-500 mr-1"></i>Đ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();
|
|
},
|
|
};
|
|
});
|