/** * PMPro Dynamic Pricing - Frontend JavaScript Optimizado v2.1 * Estructura de snippet con optimizaciones de rendimiento * * @version 2.1.0 */ jQuery(document).ready(function($) { 'use strict'; // ==================================================================== // CONFIGURACIÓN Y VARIABLES GLOBALES // ==================================================================== var PLUGIN_VERSION = '2.1.0'; var DEBUG_MODE = typeof console !== 'undefined' && (typeof nextcloud_pricing !== 'undefined' && nextcloud_pricing.debug); var CACHE_EXPIRY = 60000; // 1 minuto var TOOLTIP_DELAY = 300; // Cache simple para optimización var priceCache = {}; var originalTextsCache = {}; var activeTooltip = null; var showTooltipTimer = null; var hideTooltipTimer = null; // ==================================================================== // SISTEMA DE LOGGING // ==================================================================== function log(level, message, data) { if (!DEBUG_MODE) return; var prefix = '[PMPro Dynamic ' + level + ']'; if (data && typeof data === 'object') { console.log(prefix, message, data); } else { console.log(prefix, message); } } function logError(message, data) { log('ERROR', message, data); } function logWarn(message, data) { log('WARN', message, data); } function logInfo(message, data) { log('INFO', message, data); } function logDebug(message, data) { log('DEBUG', message, data); } // ==================================================================== // VALIDACIÓN DE DEPENDENCIAS // ==================================================================== function validateDependencies() { var checks = { nextcloud_pricing: typeof nextcloud_pricing !== 'undefined', jquery: typeof $ !== 'undefined', required_elements: $('#storage_space, #office_suite, #payment_frequency').length >= 3 }; var missing = []; for (var check in checks) { if (!checks[check]) { missing.push(check); } } if (missing.length > 0) { logError('Missing dependencies', { missing: missing }); return false; } logInfo('Dependencies validated successfully'); return true; } // ==================================================================== // CONFIGURACIÓN DINÁMICA // ==================================================================== function getConfig() { if (typeof nextcloud_pricing === 'undefined') { logError('nextcloud_pricing configuration not available'); return null; } return { levelId: nextcloud_pricing.level_id || 1, basePrice: parseInt(nextcloud_pricing.base_price) || 0, currencySymbol: nextcloud_pricing.currency_symbol || 'R$', currentStorage: nextcloud_pricing.current_storage || '1tb', currentSuite: nextcloud_pricing.current_suite || '20users', usedSpaceTb: parseFloat(nextcloud_pricing.used_space_tb) || 0, pricePerTb: 120, officeUserPrice: 25, frequencyMultipliers: { 'monthly': 1.0, 'semiannual': 5.7, 'annual': 10.8, 'biennial': 20.4, 'triennial': 28.8, 'quadrennial': 36.0, 'quinquennial': 42.0 } }; } // ==================================================================== // SISTEMA DE CACHÉ // ==================================================================== function getCachedPrice(key) { var cached = priceCache[key]; if (cached && (Date.now() - cached.timestamp) < CACHE_EXPIRY) { logDebug('Cache hit for price', { key: key }); return cached.value; } return null; } function setCachedPrice(key, value) { priceCache[key] = { value: value, timestamp: Date.now() }; logDebug('Price cached', { key: key }); } function clearPriceCache() { priceCache = {}; logDebug('Price cache cleared'); } // ==================================================================== // SISTEMA DE TOOLTIPS OPTIMIZADO // ==================================================================== var tooltipContent = { 'office-suite-tooltip': '

Integração do Collabora Online com o Nextcloud.

' + '

Collabora Online Development Edition (CODE) suporta uma média de 20 usuários simultaneos.

' + '

Para um número maior de usuários, e para evitar instabilidade ou perda de documentos, você deve selecionar uma licença:

' + '

• Collabora Online for Business (até 99 usuários)
' + '• Collabora Online for Enterprise (≥ 100 usuários)

' + '

ATENÇÃO: O número de usuários suportados pelo Collabora Online não limita o número de usuários suportados pelo Nextcloud.

' }; function createTooltip() { if (activeTooltip && activeTooltip.length) { activeTooltip.remove(); } activeTooltip = $('').css({ position: 'absolute', background: '#333', color: 'white', padding: '16px 20px', borderRadius: '8px', fontSize: '14px', lineHeight: '1.6', width: '420px', maxWidth: '95vw', zIndex: 10001, opacity: 0, visibility: 'hidden', transition: 'opacity 0.3s ease, visibility 0.3s ease', boxShadow: '0 8px 24px rgba(0, 0, 0, 0.2)', wordWrap: 'break-word', whiteSpace: 'normal', textAlign: 'left', boxSizing: 'border-box' }); $('body').append(activeTooltip); return activeTooltip; } function positionTooltip(tooltip, trigger) { var triggerOffset = trigger.offset(); var triggerWidth = trigger.outerWidth(); var triggerHeight = trigger.outerHeight(); var windowWidth = $(window).width(); var windowHeight = $(window).height(); var scrollTop = $(window).scrollTop(); // Mostrar temporalmente para obtener dimensiones tooltip.css({ visibility: 'visible', opacity: 0 }); var tooltipWidth = tooltip.outerWidth(); var tooltipHeight = tooltip.outerHeight(); // Calcular posición horizontal var left = triggerOffset.left + (triggerWidth / 2) - (tooltipWidth / 2); // Ajustar si se sale de la pantalla if (left + tooltipWidth > windowWidth - 20) { left = windowWidth - tooltipWidth - 20; } if (left < 20) { left = 20; } // Calcular posición vertical var top = triggerOffset.top - tooltipHeight - 12; // Si no cabe arriba, mostrarlo abajo if (top < scrollTop + 20) { top = triggerOffset.top + triggerHeight + 12; } // Aplicar posición tooltip.css({ left: left + 'px', top: top + 'px', visibility: 'visible', opacity: 1 }); logDebug('Tooltip positioned', { left: left, top: top }); } function showTooltip(trigger, contentKey) { clearTimeout(hideTooltipTimer); if (showTooltipTimer) { clearTimeout(showTooltipTimer); } showTooltipTimer = setTimeout(function() { var content = tooltipContent[contentKey]; if (!content) { logWarn('No content found for tooltip', { contentKey: contentKey }); return; } var tooltip = createTooltip(); tooltip.html(content); positionTooltip(tooltip, trigger); logDebug('Tooltip shown', { contentKey: contentKey }); }, TOOLTIP_DELAY); } function hideTooltip() { clearTimeout(showTooltipTimer); if (hideTooltipTimer) { clearTimeout(hideTooltipTimer); } hideTooltipTimer = setTimeout(function() { if (activeTooltip && activeTooltip.length) { activeTooltip.css({ opacity: 0, visibility: 'hidden' }); setTimeout(function() { if (activeTooltip && activeTooltip.length) { activeTooltip.remove(); activeTooltip = null; } }, 300); } logDebug('Tooltip hidden'); }, 200); } function initTooltips() { logDebug('Initializing tooltip system'); // Limpiar eventos previos $(document).off('mouseenter.tooltip mouseleave.tooltip keydown.tooltip'); $(window).off('resize.tooltip scroll.tooltip'); // Eventos de tooltip $(document).on('mouseenter.tooltip', '.pmpro-tooltip-trigger', function(e) { var $trigger = $(e.currentTarget); var tooltipId = $trigger.data('tooltip-id'); if (tooltipId && tooltipContent[tooltipId]) { showTooltip($trigger, tooltipId); } }); $(document).on('mouseleave.tooltip', '.pmpro-tooltip-trigger', function() { hideTooltip(); }); // Cerrar con Escape $(document).on('keydown.tooltip', function(e) { if (e.key === 'Escape' || e.keyCode === 27) { hideTooltip(); } }); // Reposicionar en scroll/resize $(window).on('resize.tooltip scroll.tooltip', function() { if (activeTooltip && activeTooltip.css('opacity') == '1') { hideTooltip(); } }); var triggerCount = $('.pmpro-tooltip-trigger').length; logInfo('Tooltip system initialized', { triggerCount: triggerCount }); } // ==================================================================== // GESTIÓN DE PRECIOS // ==================================================================== function getStoragePrices(config) { var prices = {}; var options = ['1tb', '2tb', '3tb', '4tb', '5tb', '6tb', '7tb', '8tb', '9tb', '10tb', '15tb', '20tb', '30tb', '40tb', '50tb', '60tb', '70tb', '80tb', '90tb', '100tb', '200tb', '300tb', '400tb', '500tb']; for (var i = 0; i < options.length; i++) { var option = options[i]; var tb = parseInt(option.replace('tb', '')); prices[option] = config.basePrice + (config.pricePerTb * Math.max(0, tb - 1)); } return prices; } function getOfficePrices(config) { return { '20users': 0, '30users': config.officeUserPrice * 30, '50users': config.officeUserPrice * 50, '80users': config.officeUserPrice * 80, '100users': (config.officeUserPrice - 3.75) * 100, '150users': (config.officeUserPrice - 3.75) * 150, '200users': (config.officeUserPrice - 3.75) * 200, '300users': (config.officeUserPrice - 3.75) * 300, '400users': (config.officeUserPrice - 3.75) * 400, '500users': (config.officeUserPrice - 3.75) * 500 }; } function formatPrice(price, currencySymbol) { var formatted = Math.ceil(price).toFixed(2) .replace('.', ',') .replace(/(\d)(?=(\d{3})+\,)/g, '$1.'); return currencySymbol + ' ' + formatted; } function getPeriodText(frequency) { var periods = { 'monthly': ' (por mês)', 'semiannual': ' (por 6 meses)', 'annual': ' (por ano)', 'biennial': ' (por 2 anos)', 'triennial': ' (por 3 anos)', 'quadrennial': ' (por 4 anos)', 'quinquennial': ' (por 5 anos)' }; return periods[frequency] || ''; } function calculateTotalPrice(config) { var storageValue = $('#storage_space').val() || config.currentStorage; var officeValue = $('#office_suite').val() || config.currentSuite; var frequencyValue = $('#payment_frequency').val() || 'monthly'; // Verificar caché var cacheKey = storageValue + '_' + officeValue + '_' + frequencyValue + '_' + config.basePrice; var cached = getCachedPrice(cacheKey); if (cached !== null) { return cached; } var storagePrices = getStoragePrices(config); var officePrices = getOfficePrices(config); var storagePrice = storagePrices[storageValue] || config.basePrice; var officePrice = officePrices[officeValue] || 0; var multiplier = config.frequencyMultipliers[frequencyValue] || 1.0; var calculation = { storageValue: storageValue, officeValue: officeValue, frequencyValue: frequencyValue, storagePrice: storagePrice, officePrice: officePrice, multiplier: multiplier, totalPrice: Math.ceil((storagePrice + officePrice) * multiplier) }; setCachedPrice(cacheKey, calculation); logDebug('Price calculated', calculation); return calculation; } function updatePriceDisplay(config) { try { var calculation = calculateTotalPrice(config); var formattedPrice = formatPrice(calculation.totalPrice, config.currencySymbol); var periodText = getPeriodText(calculation.frequencyValue); var displayText = formattedPrice + periodText; var $display = $('#total_price_display'); if ($display.length) { $display.val(displayText); logDebug('Price display updated', { displayText: displayText }); } // Trigger evento personalizado $(document).trigger('pmpropricing:updated', [calculation]); } catch (error) { logError('Error updating price display', { error: error.message }); } } // ==================================================================== // GESTIÓN DE OPCIONES // ==================================================================== function storeOriginalTexts() { $('#office_suite option, #storage_space option, #payment_frequency option').each(function() { var $option = $(this); var selectId = $option.closest('select').attr('id'); var key = selectId + '_' + $option.val(); originalTextsCache[key] = $option.text(); }); logDebug('Original texts stored', { count: Object.keys(originalTextsCache).length }); } function updateStorageOptions(config) { var currentTb = parseInt(config.currentStorage.replace('tb', '')) || 1; var $storageSelect = $('#storage_space'); if (!$storageSelect.length) return; $storageSelect.find('option').each(function() { var $option = $(this); var optionTb = parseInt($option.val().replace('tb', '')); var originalKey = 'storage_space_' + $option.val(); var originalText = originalTextsCache[originalKey] || $option.text(); // Limpiar texto previo var cleanText = originalText.replace(/ \(.*\)$/, ''); if (optionTb < currentTb && optionTb < config.usedSpaceTb) { $option.prop('disabled', true); $option.text(cleanText + ' (Espaço insuficiente)'); } else if (optionTb < currentTb) { $option.prop('disabled', true); $option.text(cleanText + ' (Downgrade não permitido)'); } else { $option.prop('disabled', false); $option.text(cleanText); } }); // Ajustar selección si está deshabilitada if ($storageSelect.find('option:selected').prop('disabled')) { var $firstEnabled = $storageSelect.find('option:not(:disabled)').first(); if ($firstEnabled.length) { $storageSelect.val($firstEnabled.val()).trigger('change'); } } // Mostrar alerta si es necesario if (config.usedSpaceTb > currentTb) { showStorageAlert(config); } logDebug('Storage options updated', { currentTb: currentTb, usedSpaceTb: config.usedSpaceTb }); } function updateOfficeOptions(config) { var currentUsers = parseInt(config.currentSuite.replace('users', '')) || 20; var $officeSelect = $('#office_suite'); if (!$officeSelect.length) return; $officeSelect.find('option').each(function() { var $option = $(this); var optionUsers = parseInt($option.val().replace('users', '')); var originalKey = 'office_suite_' + $option.val(); var originalText = originalTextsCache[originalKey] || $option.text(); if (optionUsers < currentUsers && currentUsers > 20) { if (originalText.indexOf('(Redução de licenças)') === -1) { $option.text(originalText + ' - Redução de licenças'); } } else { $option.text(originalText); } $option.prop('disabled', false); }); logDebug('Office options updated', { currentUsers: currentUsers }); } function showStorageAlert(config) { var alertId = 'storage_alert'; if ($('#' + alertId).length) return; var alertHtml = '
' + 'Atenção: Você está usando ' + config.usedSpaceTb.toFixed(2) + ' TB de armazenamento. ' + 'Não é possível reduzir abaixo deste limite.' + '
'; $('#storage_space').before(alertHtml); logInfo('Storage alert shown', { usedSpace: config.usedSpaceTb }); } // ==================================================================== // INICIALIZACIÓN PRINCIPAL // ==================================================================== function initializePMProDynamic() { logInfo('Starting PMPro Dynamic initialization', { version: PLUGIN_VERSION }); // Verificar dependencias if (!validateDependencies()) { logError('Dependencies validation failed'); return false; } // Obtener configuración var config = getConfig(); if (!config) { logError('Configuration not available'); return false; } try { // 1. Guardar textos originales storeOriginalTexts(); // 2. Configurar valores iniciales var $storage = $('#storage_space'); var $office = $('#office_suite'); var $frequency = $('#payment_frequency'); if ($storage.length && !$storage.val()) { $storage.val(config.currentStorage); } if ($office.length && !$office.val()) { $office.val(config.currentSuite); } if ($frequency.length && !$frequency.val()) { $frequency.val('monthly'); } // 3. Actualizar opciones updateStorageOptions(config); updateOfficeOptions(config); // 4. Calcular precio inicial updatePriceDisplay(config); // 5. Inicializar tooltips initTooltips(); // 6. Configurar event listeners $('#storage_space, #office_suite, #payment_frequency') .off('change.pmpro') .on('change.pmpro', function() { logDebug('Field changed', { field: $(this).attr('id'), value: $(this).val() }); // Limpiar caché clearPriceCache(); // Actualizar precio updatePriceDisplay(config); }); // 7. Mostrar sección de precio $('.pmpro_checkout-field-price-display').show(); logInfo('PMPro Dynamic initialized successfully', { fieldsFound: $('#storage_space, #office_suite, #payment_frequency').length, tooltipsFound: $('.pmpro-tooltip-trigger').length, basePrice: config.basePrice }); return true; } catch (error) { logError('Exception during initialization', { message: error.message, stack: error.stack }); return false; } } // ==================================================================== // ESTRATEGIA DE INICIALIZACIÓN MÚLTIPLE // ==================================================================== var initializationAttempts = 0; var maxAttempts = 5; var initializationDelays = [100, 250, 500, 1000, 2000]; function attemptInitialization() { if (initializationAttempts >= maxAttempts) { logError('Maximum initialization attempts reached'); return; } var delay = initializationDelays[initializationAttempts] || 2000; initializationAttempts++; setTimeout(function() { logDebug('Initialization attempt ' + initializationAttempts + '/' + maxAttempts); if (initializePMProDynamic()) { logInfo('Initialization successful'); return; } // Si falló, intentar de nuevo if (initializationAttempts < maxAttempts) { logWarn('Initialization failed, retrying in ' + initializationDelays[initializationAttempts] + 'ms'); attemptInitialization(); } }, delay); } // Iniciar proceso attemptInitialization(); // Backup: inicializar cuando la página esté cargada $(window).on('load', function() { if ($('.pmpro-tooltip-trigger').length === 0 || $('#total_price_display').val() === '') { logInfo('Window load backup initialization'); setTimeout(initializePMProDynamic, 500); } }); // API para debugging (solo en modo debug) if (DEBUG_MODE) { window.PMProDynamic = { version: PLUGIN_VERSION, reinitialize: initializePMProDynamic, clearCache: clearPriceCache, getConfig: getConfig, log: { error: logError, warn: logWarn, info: logInfo, debug: logDebug } }; logInfo('Debug mode enabled - API available in window.PMProDynamic'); } });