From c75680d0292f426ded47d929057ca3132be8e980 Mon Sep 17 00:00:00 2001 From: DavidCamejo Date: Tue, 4 Nov 2025 21:08:40 -0400 Subject: [PATCH] Initial commit --- CHANGELOG.md | 16 + CONTRIBUTING.md | 119 + README.md | 185 ++ admin/class-admin-page.php | 326 ++ admin/class-snippet-editor.php | 330 ++ admin/views/snippet-editor.php | 107 + admin/views/snippet-list.php | 177 ++ admin/views/snippets-list.php | 177 ++ assets/css/editor.css | 39 + assets/images/banner.jpg | Bin 0 -> 20214 bytes assets/js/editor.js | 85 + includes/class-hook-detector.php | 67 + includes/class-snippet-manager.php | 495 +++ includes/class-syntax-checker.php | 66 + includes/snippets-order.php | 17 + readme.txt | 33 + simply-code.php | 39 + storage/css/auto-redirect-after-logout.css | 0 storage/css/back-to-top-button.css | 23 + storage/css/crear-plan-nextcloud-ti.css | 0 storage/css/current-year.css | 0 storage/css/custom-login-logo.css | 0 .../get-nextcloud-storage-via-reports-api.css | 0 .../css/nextcloud-banda-dynamic-pricing.css | 909 ++++++ storage/css/pmpro-dynamic-pricing.css | 824 +++++ storage/css/pmpro-hide-toolbar.css | 0 storage/css/pmpro-pricing-tables.css | 0 storage/css/simple-accordion.css | 127 + storage/css/smtp-settings.css | 0 storage/css/theme-scripts.css | 1 + storage/js/auto-redirect-after-logout.js | 0 storage/js/back-to-top-button.js | 14 + storage/js/crear-plan-nextcloud-ti.js | 0 storage/js/current-year.js | 0 storage/js/custom-login-logo.js | 0 .../get-nextcloud-storage-via-reports-api.js | 0 storage/js/nextcloud-banda-dynamic-pricing.js | 1428 +++++++++ storage/js/pmpro-dynamic-pricing.js | 660 ++++ storage/js/pmpro-hide-toolbar.js | 0 storage/js/pmpro-pricing-tables.js | 0 storage/js/simple-accordion.js | 53 + storage/js/smtp-settings.js | 0 storage/js/theme-scripts.js | 1 + .../snippets/auto-redirect-after-logout.json | 14 + .../snippets/auto-redirect-after-logout.php | 12 + storage/snippets/back-to-top-button.json | 7 + storage/snippets/back-to-top-button.php | 3 + storage/snippets/crear-plan-nextcloud-ti.json | 7 + storage/snippets/crear-plan-nextcloud-ti.php | 420 +++ storage/snippets/current-year.json | 7 + storage/snippets/current-year.php | 9 + storage/snippets/custom-login-logo.json | 14 + storage/snippets/custom-login-logo.php | 9 + ...get-nextcloud-storage-via-reports-api.json | 20 + .../get-nextcloud-storage-via-reports-api.php | 67 + .../nextcloud-banda-dynamic-pricing.json | 50 + .../nextcloud-banda-dynamic-pricing.php | 2763 +++++++++++++++++ storage/snippets/pmpro-dynamic-pricing.json | 98 + storage/snippets/pmpro-dynamic-pricing.php | 995 ++++++ storage/snippets/simple-accordion.json | 7 + storage/snippets/simple-accordion.php | 55 + storage/snippets/smtp-settings.json | 14 + storage/snippets/smtp-settings.php | 16 + storage/snippets/theme-scripts.json | 26 + storage/snippets/theme-scripts.php | 382 +++ templates/class.php | 11 + templates/configuration.php | 6 + templates/function.php | 5 + templates/hook.php | 5 + 69 files changed, 11340 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 admin/class-admin-page.php create mode 100644 admin/class-snippet-editor.php create mode 100644 admin/views/snippet-editor.php create mode 100644 admin/views/snippet-list.php create mode 100644 admin/views/snippets-list.php create mode 100644 assets/css/editor.css create mode 100644 assets/images/banner.jpg create mode 100644 assets/js/editor.js create mode 100644 includes/class-hook-detector.php create mode 100644 includes/class-snippet-manager.php create mode 100644 includes/class-syntax-checker.php create mode 100644 includes/snippets-order.php create mode 100644 readme.txt create mode 100644 simply-code.php create mode 100644 storage/css/auto-redirect-after-logout.css create mode 100644 storage/css/back-to-top-button.css create mode 100644 storage/css/crear-plan-nextcloud-ti.css create mode 100644 storage/css/current-year.css create mode 100644 storage/css/custom-login-logo.css create mode 100644 storage/css/get-nextcloud-storage-via-reports-api.css create mode 100644 storage/css/nextcloud-banda-dynamic-pricing.css create mode 100644 storage/css/pmpro-dynamic-pricing.css create mode 100644 storage/css/pmpro-hide-toolbar.css create mode 100644 storage/css/pmpro-pricing-tables.css create mode 100644 storage/css/simple-accordion.css create mode 100644 storage/css/smtp-settings.css create mode 100644 storage/css/theme-scripts.css create mode 100644 storage/js/auto-redirect-after-logout.js create mode 100644 storage/js/back-to-top-button.js create mode 100644 storage/js/crear-plan-nextcloud-ti.js create mode 100644 storage/js/current-year.js create mode 100644 storage/js/custom-login-logo.js create mode 100644 storage/js/get-nextcloud-storage-via-reports-api.js create mode 100644 storage/js/nextcloud-banda-dynamic-pricing.js create mode 100644 storage/js/pmpro-dynamic-pricing.js create mode 100644 storage/js/pmpro-hide-toolbar.js create mode 100644 storage/js/pmpro-pricing-tables.js create mode 100644 storage/js/simple-accordion.js create mode 100644 storage/js/smtp-settings.js create mode 100644 storage/js/theme-scripts.js create mode 100644 storage/snippets/auto-redirect-after-logout.json create mode 100644 storage/snippets/auto-redirect-after-logout.php create mode 100644 storage/snippets/back-to-top-button.json create mode 100644 storage/snippets/back-to-top-button.php create mode 100644 storage/snippets/crear-plan-nextcloud-ti.json create mode 100644 storage/snippets/crear-plan-nextcloud-ti.php create mode 100644 storage/snippets/current-year.json create mode 100644 storage/snippets/current-year.php create mode 100644 storage/snippets/custom-login-logo.json create mode 100644 storage/snippets/custom-login-logo.php create mode 100644 storage/snippets/get-nextcloud-storage-via-reports-api.json create mode 100644 storage/snippets/get-nextcloud-storage-via-reports-api.php create mode 100644 storage/snippets/nextcloud-banda-dynamic-pricing.json create mode 100644 storage/snippets/nextcloud-banda-dynamic-pricing.php create mode 100644 storage/snippets/pmpro-dynamic-pricing.json create mode 100644 storage/snippets/pmpro-dynamic-pricing.php create mode 100644 storage/snippets/simple-accordion.json create mode 100644 storage/snippets/simple-accordion.php create mode 100644 storage/snippets/smtp-settings.json create mode 100644 storage/snippets/smtp-settings.php create mode 100644 storage/snippets/theme-scripts.json create mode 100644 storage/snippets/theme-scripts.php create mode 100644 templates/class.php create mode 100644 templates/configuration.php create mode 100644 templates/function.php create mode 100644 templates/hook.php diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..52bc0d6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +Todos los cambios notables de este proyecto se documentarán en este archivo. + +## [3.0] - 2024-07-28 +### Añadido +- Versión inicial pública del plugin. +- Creación automática de carpetas necesarias en la activación. +- Gestión modular de snippets (PHP, JS, CSS) mediante archivos. +- Validación de sintaxis antes de guardar snippets. +- Interfaz de administración para crear, editar, activar/desactivar y eliminar snippets. +- Soporte para backups automáticos y restauración sencilla. +- Compatibilidad con WordPress Multisite. +- Hooks principales para carga de snippets y administración. + +--- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b66160e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,119 @@ +# Guía de Contribución para Simply Code + +¡Gracias por tu interés en contribuir a Simply Code! Tu ayuda es fundamental para mejorar este plugin y su comunidad. + +## Formas de contribuir + +- Reportar errores o problemas +- Sugerir nuevas características +- Mejorar la documentación +- Enviar código (nuevas funciones, correcciones, mejoras) +- Compartir nuevos snippets útiles + +--- + +## Reportar errores + +Si encuentras un bug, por favor abre un "Issue" en GitHub e incluye: + +1. Pasos para reproducir el problema +2. Comportamiento esperado y comportamiento actual +3. Versión de Simply Code, WordPress y PHP +4. Capturas de pantalla o videos si es posible + +--- + +## Sugerir características + +¿Tienes una idea para mejorar el plugin? Abre un "Issue" y describe: + +- El problema que resuelve tu propuesta +- Cómo debería funcionar la nueva característica +- Por qué sería útil para otros usuarios + +--- + +## Enviar código (Pull Requests) + +1. Haz un fork del repositorio y clónalo en tu equipo. +2. Crea una rama para tu cambio: + ```bash + git checkout -b feature/nombre-o-bugfix/descripcion + ``` +3. Realiza tus cambios siguiendo los [estándares de codificación de WordPress](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/). +4. Prueba tu código antes de hacer commit. +5. Escribe mensajes de commit claros y descriptivos. +6. Sincroniza tu rama con `main` antes de enviar el Pull Request. +7. Abre un Pull Request en GitHub, describe tus cambios y referencia cualquier Issue relacionado. + +--- + +## Contribuir con nuevos snippets + +¿Tienes un snippet útil de PHP, JavaScript o CSS? ¡Compártelo con la comunidad! + +### ¿Qué tipo de snippets puedes enviar? + +- Funciones personalizadas para WordPress +- Hooks y filtros útiles +- Fragmentos de JavaScript para mejorar la experiencia de usuario +- Estilos CSS para personalización rápida +- Soluciones a problemas comunes o utilidades generales + +### ¿Cómo enviar tu snippet? + +1. **Formato:** + - Envía tu snippet como un archivo independiente en la carpeta `community-snippets/` (o la que se indique en el repositorio). + - Usa la extensión adecuada: `.php`, `.js` o `.css`. + - Incluye al inicio del archivo un bloque de comentarios con: + ``` + /* + * Nombre: Nombre descriptivo del snippet + * Descripción: Explica brevemente qué hace el snippet y en qué casos es útil. + * Autor: Tu nombre o usuario de GitHub + * Requisitos: (opcional) Versión mínima de WordPress, plugins necesarios, etc. + */ + ``` +2. **Calidad y seguridad:** + - El código debe ser seguro, funcional y seguir los [estándares de codificación de WordPress](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/). + - No incluyas datos sensibles ni dependencias externas no revisadas. +3. **Pull Request:** + - Haz un fork del repositorio y crea una rama para tu snippet. + - Sube tu archivo a la carpeta `community-snippets/`. + - Abre un Pull Request describiendo la utilidad del snippet y cualquier detalle relevante. +4. **Revisión:** + - El equipo revisará tu snippet para asegurar su calidad y seguridad antes de aceptarlo. + - Si es aceptado, tu snippet será incluido en la biblioteca de Simply Code y se te dará crédito como autor/a. + +#### Ejemplo de snippet enviado + +```php +/* + * Nombre: Desactivar comentarios en medios + * Descripción: Evita que los usuarios puedan comentar en archivos adjuntos (medios). + * Autor: @ejemplo + */ +add_filter('comments_open', function($open, $post_id) { + $post = get_post($post_id); + if ($post->post_type === 'attachment') { + return false; + } + return $open; +}, 10, 2); +``` + +--- + +## Estándares de codificación + +Por favor, adhiérete a los [estándares de codificación de WordPress](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/) para PHP, JavaScript y CSS. Esto ayuda a mantener la consistencia y legibilidad del código. + +--- + +## Licencia + +Al contribuir con código o snippets a Simply Code, aceptas que tu contribución se licenciará bajo la misma [Licencia GPL v2 o posterior](https://www.gnu.org/licenses/gpl-2.0.html) que el proyecto. + +--- + +¡Gracias por ayudar a hacer Simply Code mejor para todos! diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5e9453 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# Simply Code + +![Simply Code Banner](assets/images/banner.jpg) + +Simply Code es un plugin de WordPress minimalista y eficiente que moderniza la forma de gestionar código personalizado en tu sitio. Diseñado como una alternativa modular y profesional a `functions.php`, permite administrar snippets de código como módulos independientes. A diferencia de otras soluciones, Simply Code utiliza un sistema de archivos directo en lugar de la base de datos, lo que resulta en mejor rendimiento y mayor portabilidad. + +## ¿Por qué Simply Code? + +- 📂 **Alternativa moderna a functions.php**: Organiza tu código en módulos independientes +- 🔄 **Activación/desactivación instantánea**: Prueba cambios sin modificar archivos del tema +- 🚀 **Sin dependencia de base de datos**: Mejor rendimiento y facilidad de migración +- 👥 **Ideal para equipos**: Facilita la colaboración y el control de versiones +- 🔒 **Modo seguro**: Validación de sintaxis antes de guardar y ejecutar código +- 🎨 **Soporte completo**: PHP, JavaScript y CSS en cada snippet + +## Instalación + +1. Descarga el archivo ZIP del plugin +2. Ve a tu panel de WordPress > Plugins > Añadir nuevo +3. Haz clic en "Subir Plugin" y selecciona el archivo ZIP +4. Activa el plugin + +## Estructura de archivos + +``` +simply-code/ +├── admin/ +│ ├── class-admin-page.php +│ ├── class-snippet-editor.php +│ └── views/ +│ ├── snippet-editor.php +│ └── snippets-list.php +├── assets/ +│ ├── css/ +│ │ └── editor.css +│ ├── js/ +│ │ └── editor.js +│ └── images/ +│ └── banner.jpg +├── includes/ +│ ├── class-snippet-manager.php +│ ├── class-syntax-checker.php +│ └── snippets-order.php +├── storage/ +│ ├── snippets/ +│ ├── js/ +│ ├── css/ +│ └── backups/ +├── templates/ +│ ├── class.php +│ ├── configuration.php +│ ├── function.php +│ └── hook.php +├── CHANGELOG.md +├── CONTRIBUTING.md +├── README.md +├── readme.txt +└── simply-code.php +``` + +## Uso + +### Crear un nuevo snippet + +1. Ve a "Simply Code" en el menú de WordPress +2. Haz clic en "Nuevo Snippet" +3. Completa los campos: + - Nombre del snippet + - Descripción + - Código PHP, JavaScript y/o CSS +4. Guarda el snippet + +### Ejemplos de snippets + +```php +// Función personalizada +function mi_funcion_util() { + // Tu código aquí +} + +// Clase personalizada +class Mi_Clase_Personalizada { + public function __construct() { + // Inicialización + } +} + +// Hook de WordPress +add_action('init', function() { + // Código a ejecutar +}); + +// Configuración personalizada +define('MI_CONSTANTE', 'valor'); +``` +```js +// JavaScript: Mostrar alerta en el frontend +document.addEventListener('DOMContentLoaded', function() { + alert('¡Hola desde Simply Code!'); +}); +``` +```css +/* CSS: Cambiar color de fondo del body */ +body { + background-color: #f5f5f5; +} +``` + +### Gestión de snippets + +- **Activar/Desactivar**: Usa el interruptor en la lista de snippets +- **Ordenar**: Utiliza las flechas arriba/abajo para cambiar el orden de ejecución +- **Editar**: Modifica el código y configuración de cualquier snippet existente +- **Eliminar**: Elimina snippets que ya no necesites + +## Ventajas del sistema basado en archivos + +1. **Mejor rendimiento**: Sin consultas a la base de datos +2. **Mayor portabilidad**: Fácil de migrar entre instalaciones +3. **Control de versiones**: Los snippets pueden versionarse con Git +4. **Backups simplificados**: Copias de seguridad automáticas y fáciles de restaurar +5. **Depuración sencilla**: Los archivos son fáciles de inspeccionar y debuggear + +## Modo seguro + +El modo seguro realiza las siguientes validaciones: + +- Comprueba la sintaxis PHP antes de guardar +- Valida la estructura del código +- Previene errores que podrían romper el sitio + +## Características técnicas + +- **Versión mínima de PHP**: 7.4 +- **Versión mínima de WordPress**: 5.6 +- **Licencia**: GPL v2 o posterior +- **Requiere privilegios**: `manage_options` + +## Detalles técnicos recientes + +- El plugin crea automáticamente las carpetas necesarias (`storage/snippets/`, `storage/js/`, `storage/css/`, `templates/`) si no existen, asegurando que el entorno esté listo desde la activación. +- El sistema de carga modular utiliza clases separadas para la gestión de snippets, validación de sintaxis y la interfaz de administración. +- Los hooks principales registrados son: + - `after_setup_theme` para cargar los snippets al inicio. + - `admin_menu` para registrar el menú de administración. + - `wp_enqueue_scripts` para cargar los assets de los snippets en el frontend. +- El almacenamiento de los snippets y recursos asociados se realiza exclusivamente en el sistema de archivos, nunca en la base de datos. + +Consulta el archivo CHANGELOG.md para un historial detallado de cambios y mejoras. + +## FAQ + +### ¿Por qué usar Simply Code en lugar de functions.php? + +Simply Code ofrece una gestión modular del código, con interfaz gráfica profesional y la capacidad de activar/desactivar snippets individualmente. Además, mantiene tu código organizado y facilita la colaboración en equipo. + +### ¿Cómo migro mis snippets a otra instalación? + +Simplemente copia el contenido de la carpeta `storage/` y el archivo `includes/snippets-order.php` a la nueva instalación. Al estar basado en archivos, la migración es sencilla y directa. + +### ¿Se pierden los snippets al actualizar el plugin? + +No. Simply Code mantiene los snippets en una ubicación separada y crea backups automáticos antes de las actualizaciones. + +### ¿Puedo usar Simply Code en un entorno multisite? + +Sí, Simply Code es compatible con WordPress multisite. Cada sitio puede tener sus propios snippets independientes. + +## Contribuir + +Las contribuciones son bienvenidas. Por favor, revisa las [guías de contribución](CONTRIBUTING.md) antes de enviar un pull request. + +## Soporte + +- 📝 [Documentación](docs/README.md) +- 🐛 [Reportar un problema](../../issues) +- 💡 [Sugerir una característica](../../issues/new?template=feature_request.md) + +## Licencia + +Simply Code está licenciado bajo la GPL v2 o posterior. Consulta el archivo [LICENSE](LICENSE) para más detalles. + +## Créditos + +Desarrollado por David Camejo & AI diff --git a/admin/class-admin-page.php b/admin/class-admin-page.php new file mode 100644 index 0000000..d86eb87 --- /dev/null +++ b/admin/class-admin-page.php @@ -0,0 +1,326 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('simply_code_detect_hooks') + ]); + } + + /** + * Handle admin notices - VERSIÓN MEJORADA CON TRANSIENTS + */ + private static function handle_admin_notices() { + $notice = ''; + + // Manejar mensajes de éxito desde transients + $success_message = get_transient('simply_code_success'); + if ($success_message) { + $notice .= '

' . esc_html($success_message) . '

'; + delete_transient('simply_code_success'); + } + + // Manejar mensajes de error desde transients + $error_message = get_transient('simply_code_error'); + if ($error_message) { + $notice .= '

' . esc_html($error_message) . '

'; + delete_transient('simply_code_error'); + } + + // Mantener compatibilidad con parámetros GET existentes + if (isset($_GET['created']) && isset($_GET['snippet'])) { + $snippet_name = sanitize_text_field(urldecode($_GET['snippet'])); + $notice .= sprintf( + '

Snippet "%s" creado correctamente.

', + esc_html($snippet_name) + ); + } + + if (isset($_GET['updated']) && isset($_GET['snippet'])) { + $snippet_name = sanitize_text_field(urldecode($_GET['snippet'])); + $notice .= sprintf( + '

Snippet "%s" actualizado correctamente.

', + esc_html($snippet_name) + ); + } + + if (isset($_GET['deleted']) && isset($_GET['snippet'])) { + $snippet_name = sanitize_text_field(urldecode($_GET['snippet'])); + $notice .= sprintf( + '

Snippet "%s" eliminado correctamente.

', + esc_html($snippet_name) + ); + } + + if (isset($_GET['saved'])) { + $message = 'Snippet guardado correctamente.'; + if (isset($_GET['snippet_name'])) { + $snippet_name = sanitize_text_field(urldecode($_GET['snippet_name'])); + $message = sprintf('Snippet "%s" guardado correctamente.', esc_html($snippet_name)); + } + $notice .= sprintf( + '

%s

', + $message + ); + } + + if (isset($_GET['status_changed']) && isset($_GET['snippet']) && isset($_GET['status'])) { + $snippet_name = sanitize_text_field(urldecode($_GET['snippet'])); + $status = $_GET['status'] === 'activated' ? 'activado' : 'desactivado'; + $notice .= sprintf( + '

Snippet "%s" %s correctamente.

', + esc_html($snippet_name), + $status + ); + } + + if (isset($_GET['reordered'])) { + $notice .= '

Orden de snippets actualizado correctamente.

'; + } + + if (isset($_GET['safe_mode_updated'])) { + $notice .= '

Configuración de modo seguro actualizada correctamente.

'; + } + + return $notice; + } + + /** + * Safe redirect with proper buffer cleanup + */ + private static function safe_redirect($url) { + // Solo limpiar si hay buffers de usuario con contenido + if (ob_get_level() > 0 && ob_get_contents() !== false) { + $buffer_content = ob_get_contents(); + if (!empty(trim($buffer_content))) { + @ob_end_clean(); + } + } + + // Verificar que no se hayan enviado headers + if (headers_sent($file, $line)) { + error_log("Simply Code: Headers already sent in {$file} at line {$line}, cannot redirect to: {$url}"); + echo ''; + echo ''; + exit; + } + + // Usar wp_safe_redirect que es más robusto + $result = wp_safe_redirect($url); + if (!$result) { + error_log("Simply Code: wp_safe_redirect failed for URL: {$url}"); + echo ''; + } + exit; + } + + /** + * Main admin page handler + */ + public static function main_page() { + $action_message = ''; + + // Verificar permisos + if (!current_user_can('manage_options')) { + wp_die(__('No tienes permisos suficientes para acceder a esta página.')); + } + + // CRÍTICO: Manejar POST antes de cualquier output + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (empty($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'simply_code_actions')) { + wp_die('Error de seguridad'); + } + + // Solo delete requiere redirect (para evitar resubmisión accidental) + if (isset($_POST['delete_snippet'])) { + self::handle_deletion(); // Hace redirect y exit + } + + // Otras acciones sin redirect para evitar el efecto "doble carga" + if (isset($_POST['toggle_snippet_status'])) { + $action_message = self::handle_status_toggle_inline(); + } + elseif (isset($_POST['move_up']) || isset($_POST['move_down'])) { + $action_message = self::handle_reordering_inline(); + } + elseif (isset($_POST['safe_mode_toggle'])) { + $action_message = self::handle_safe_mode_inline(); + } + else { + // Si no coincide con ninguna acción conocida + $action_message = 'Acción POST no reconocida'; + } + } + + // Mostrar notices de URL parameters + $notice = self::handle_admin_notices(); + + // Agregar mensaje de acción inline si existe + if ($action_message) { + $notice .= '

' . esc_html($action_message) . '

'; + } + + // Obtener snippets + $snippets = Simply_Snippet_Manager::list_snippets(true, true); + + // Mostrar notices + if ($notice) { + echo wp_kses_post($notice); + } + + // Renderizar vista + include SC_PATH . 'admin/views/snippets-list.php'; + } + + /** + * Handle status toggle without redirect + */ + private static function handle_status_toggle_inline() { + if (!isset($_POST['toggle_snippet_status'], $_POST['snippet_name'])) { + return 'Error: Datos de formulario incompletos para cambio de estado.'; + } + + $snippet_name = sanitize_text_field($_POST['snippet_name']); + $new_status = isset($_POST['snippet_active']); + + if (Simply_Snippet_Manager::toggle_snippet_status($snippet_name, $new_status)) { + $status_text = $new_status ? 'activado' : 'desactivado'; + return sprintf('Snippet "%s" %s correctamente.', esc_html($snippet_name), $status_text); + } + + return sprintf('Error al cambiar el estado del snippet "%s".', esc_html($snippet_name)); + } + + /** + * Handle reordering without redirect + */ + private static function handle_reordering_inline() { + if (!isset($_POST['move_up']) && !isset($_POST['move_down'])) { + return 'Error: Acción de reordenamiento no especificada.'; + } + + $snippets = Simply_Snippet_Manager::list_snippets(true); + $i = isset($_POST['move_up']) ? (int)$_POST['move_up'] : (int)$_POST['move_down']; + $names = array_map(function($s) { return $s['name']; }, $snippets); + $changed = false; + + if (isset($_POST['move_up']) && $i > 0 && $i < count($names)) { + $tmp = $names[$i-1]; + $names[$i-1] = $names[$i]; + $names[$i] = $tmp; + $changed = true; + } + elseif (isset($_POST['move_down']) && $i >= 0 && $i < count($names) - 1) { + $tmp = $names[$i+1]; + $names[$i+1] = $names[$i]; + $names[$i] = $tmp; + $changed = true; + } + + if ($changed && Simply_Snippet_Manager::update_snippets_order($names)) { + return 'Orden de snippets actualizado correctamente.'; + } + + return 'Error al actualizar el orden de los snippets.'; + } + + /** + * Handle safe mode without redirect + */ + private static function handle_safe_mode_inline() { + if (!isset($_POST['safe_mode_toggle'])) { + return 'Error: Configuración de modo seguro no especificada.'; + } + + update_option( + self::OPTION_SAFE_MODE, + isset($_POST['safe_mode']) && $_POST['safe_mode'] === 'on' ? 'on' : 'off' + ); + + return 'Configuración de modo seguro actualizada correctamente.'; + } + + /** + * Handle deletion with redirect + */ + private static function handle_deletion() { + if (!isset($_POST['delete_snippet'], $_POST['snippet_name'])) { + wp_die('Error: Datos de eliminación incompletos.'); + } + + $snippet_name = sanitize_text_field($_POST['snippet_name']); + + if (Simply_Snippet_Manager::delete_snippet($snippet_name)) { + $success_message = sprintf('Snippet "%s" eliminado correctamente.', esc_html($snippet_name)); + set_transient('simply_code_success', $success_message, 45); + + $redirect_url = admin_url('admin.php?page=simply-code'); + self::safe_redirect($redirect_url); + return; // Never reached + } + + wp_die('Error al eliminar el snippet "' . esc_html($snippet_name) . '".'); + } +} diff --git a/admin/class-snippet-editor.php b/admin/class-snippet-editor.php new file mode 100644 index 0000000..fa4b476 --- /dev/null +++ b/admin/class-snippet-editor.php @@ -0,0 +1,330 @@ + $hooks, + 'critical_hooks' => $critical_hooks + ]); + } + + /** + * Sanitize form data + */ + private static function sanitize_form_data($post_data) { + $hook_priorities = []; + if (isset($post_data['hook_priorities']) && is_array($post_data['hook_priorities'])) { + foreach ($post_data['hook_priorities'] as $hook => $priority) { + $hook_priorities[sanitize_text_field($hook)] = (int)$priority; + } + } + + return [ + 'snippet_name' => sanitize_file_name($post_data['snippet_name'] ?? ''), + 'php_code' => stripslashes($post_data['php_code'] ?? ''), + 'js_code' => stripslashes($post_data['js_code'] ?? ''), + 'css_code' => stripslashes($post_data['css_code'] ?? ''), + 'description' => stripslashes($post_data['description'] ?? ''), + 'template' => sanitize_text_field($post_data['template'] ?? ''), + 'hook_priorities' => $hook_priorities + ]; + } + + /** + * Validate form data + */ + private static function validate_form_data($form_data, $edit_mode) { + // Check required fields + if (empty($form_data['snippet_name'])) { + return 'El nombre del snippet es requerido'; + } + // Validar formato del nombre para prevenir XSS + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $form_data['snippet_name'])) { + return 'El nombre del snippet solo puede contener letras, números, guiones y guiones bajos'; + } + // Check for duplicate names (creation mode only) + if (!$edit_mode && Simply_Snippet_Manager::get_snippet($form_data['snippet_name'])) { + return 'Ya existe un snippet con ese nombre'; + } + // Validate PHP syntax (si existe la clase) + if (!empty($form_data['php_code']) && class_exists('Simply_Syntax_Checker') && method_exists('Simply_Syntax_Checker', 'validate_php')) { + $syntax_result = Simply_Syntax_Checker::validate_php($form_data['php_code']); + if (isset($syntax_result['valid']) && $syntax_result['valid'] === false) { + return 'Error de sintaxis en PHP: ' . ($syntax_result['message'] ?? 'error desconocido'); + } + } + return true; + } + + /** + * Apply template to form data + */ + private static function apply_template($form_data) { + $templates = self::get_templates(); + if (isset($templates[$form_data['template']])) { + $template = $templates[$form_data['template']]; + $form_data['php_code'] = $template['code']; + if (empty($form_data['description'])) { + $form_data['description'] = $template['description']; + } + } + return $form_data; + } + + /** + * Get existing snippet active status + */ + private static function get_existing_active_status($snippet_name) { + $existing_snippet = Simply_Snippet_Manager::get_snippet($snippet_name); + return $existing_snippet ? $existing_snippet['active'] : true; + } + + /** + * Almacena un error para mostrarlo después del redirect + */ + private static function store_error($message) { + set_transient('simply_code_error', $message, 45); + } + + /** + * Muestra los errores almacenados + */ + private static function show_stored_errors() { + $error = get_transient('simply_code_error'); + if ($error) { + self::show_error($error); + delete_transient('simply_code_error'); + } + } + + /** + * Redirecciona de vuelta a la página anterior + */ + private static function redirect_back() { + $redirect_url = wp_get_referer(); + if (!$redirect_url) { + $redirect_url = admin_url('admin.php?page=simply-code'); + } + self::safe_redirect($redirect_url); + } + + /** + * Realiza un redirect seguro + */ + private static function safe_redirect($url) { + // Solo limpiar si hay buffers de usuario con contenido + if (ob_get_level() > 0 && ob_get_contents() !== false) { + $buffer_content = ob_get_contents(); + if (!empty(trim($buffer_content))) { + @ob_end_clean(); + } + } + // Verificar que no se hayan enviado headers + if (headers_sent($file, $line)) { + error_log("Simply Code: Headers already sent in {$file} at line {$line}, cannot redirect to: {$url}"); + echo ''; + exit; + } + // Usar wp_safe_redirect con verificación + $result = wp_safe_redirect($url); + if (!$result) { + error_log("Simply Code: wp_safe_redirect failed for URL: {$url}"); + echo ''; + } + exit; + } + + /** + * Redirect with success message + */ + private static function redirect_with_success($edit_mode, $snippet_name) { + $action_text = $edit_mode ? 'actualizado' : 'creado'; + $success_message = sprintf('Snippet "%s" %s correctamente.', esc_html($snippet_name), $action_text); + set_transient('simply_code_success', $success_message, 45); + $redirect_url = admin_url('admin.php?page=simply-code'); + self::safe_redirect($redirect_url); + } + + /** + * Show error message + */ + private static function show_error($message) { + echo '

' . esc_html($message) . '

'; + } + + /** + * Render the view + */ + private static function render_view($view_data) { + extract($view_data); + include SC_PATH . 'admin/views/snippet-editor.php'; + } + + /** + * Prepara los datos para la vista + */ + private static function prepare_view_data($edit_mode, $snippet_name) { + $templates = self::get_templates(); + // Valores por defecto + $php_code = $templates['empty']['code'] ?? " $templates, + 'php_code' => $php_code, + 'js_code' => $js_code, + 'css_code' => $css_code, + 'description' => $description, + 'snippet' => $snippet, + 'edit_mode' => $edit_mode, + 'hooks_data' => $hooks_data, + 'critical_hooks' => (class_exists('Simply_Hook_Detector') && method_exists('Simply_Hook_Detector', 'get_critical_hooks')) ? Simply_Hook_Detector::get_critical_hooks() : [] + ]; + } + + /** + * Get templates with caching + */ + public static function get_templates() { + if (self::$templates !== null) { + return self::$templates; + } + + self::$templates = [ + 'empty' => [ + 'code' => " 'Snippet vacío' + ], + 'function' => [ + 'code' => " 'Función personalizada' + ], + 'action' => [ + 'code' => " 'Acción de WordPress' + ], + 'filter' => [ + 'code' => " 'Filtro de WordPress' + ] + ]; + return self::$templates; + } +} diff --git a/admin/views/snippet-editor.php b/admin/views/snippet-editor.php new file mode 100644 index 0000000..280c261 --- /dev/null +++ b/admin/views/snippet-editor.php @@ -0,0 +1,107 @@ + +
+

+ + +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

Sólo letras, números, guiones y guiones bajos.

+
+ +
+ +
PHP + +
JavaScript + +
CSS + +
Hooks detectados +
+ + $hook_info): ?> +
+ + +
+ Prioridad: + + ⚠️ Hook crítico + +
+ + +

Los hooks se detectan automáticamente. Puedes usar el botón para detectarlos.

+ +
+ +

+ + +

+ + + +
+ + +
+
+ + +'; +?> diff --git a/admin/views/snippet-list.php b/admin/views/snippet-list.php new file mode 100644 index 0000000..2234f14 --- /dev/null +++ b/admin/views/snippet-list.php @@ -0,0 +1,177 @@ +
+

Simply Code

+ + + + + +
+
+
+ + + +
+
+ +
+
+ + + + + + + + + + + + $snippet): ?> + + + + + + + + + + + + + +
NombreDescripciónEstadoAcciones
+
+ + + + +
+
+ 0): ?> +
+ + + +
+ + + +
+ + + +
+ + + + +
+ + + + +
+
No hay snippets disponibles.
+
+ + diff --git a/admin/views/snippets-list.php b/admin/views/snippets-list.php new file mode 100644 index 0000000..2234f14 --- /dev/null +++ b/admin/views/snippets-list.php @@ -0,0 +1,177 @@ +
+

Simply Code

+ + + + + +
+
+
+ + + +
+
+ +
+
+ + + + + + + + + + + + $snippet): ?> + + + + + + + + + + + + + +
NombreDescripciónEstadoAcciones
+
+ + + + +
+
+ 0): ?> +
+ + + +
+ + + +
+ + + +
+ + + + +
+ + + + +
+
No hay snippets disponibles.
+
+ + diff --git a/assets/css/editor.css b/assets/css/editor.css new file mode 100644 index 0000000..b8d5b9e --- /dev/null +++ b/assets/css/editor.css @@ -0,0 +1,39 @@ +.sc-editor-tabs ul { + list-style: none; + padding: 0; + margin: 20px 0 0; + border-bottom: 1px solid #ccc; +} + +.sc-editor-tabs li { + display: inline-block; + padding: 10px 20px; + margin: 0; + cursor: pointer; + border: 1px solid transparent; + border-bottom: none; + margin-bottom: -1px; +} + +.sc-editor-tabs li.active { + background: #fff; + border-color: #ccc; + border-bottom-color: #fff; +} + +.tab-content { + display: none; + padding: 20px; + border: 1px solid #ccc; + border-top: none; +} + +.tab-content.active { + display: block; +} + +.tab-content textarea { + width: 100%; + margin: 0; + resize: vertical; +} \ No newline at end of file diff --git a/assets/images/banner.jpg b/assets/images/banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0990a2ce59f034e24b3a44884124268fafbeb772 GIT binary patch literal 20214 zcmeIac|4Tu_c(sf7((_fgvJ_WU&hYZ_v}O|gTdJMH5r6Rk$op?Xi@f}4K0LhS(Av! z5@jhR-)pAjd7kh4d7j_v_xgNa-`^j<_tic3eXetzbFOop`2+kRT^z@EE%X2ldPXKlNl^)LQ7JhfMO;!w zL0ncr5`~nIR*;cZkd}lToSkGO<>VzC#Kol~Bpjr$&f*v+IVTA@loZwhgO!t(mvWLp zVdcfeF^(u%IVlG$km4W-*c_alu`*aff3V^|Mn=$L?+sOtYe0aHf|!_>zbMAZ+W{-; z=7+4_3?!>+c=t=ZMt`!g>Yp{$h7>{LSkV=;uKkfRm#b)&uJa z$ozrDC4P5B@Q2^jgaNzYJbj29z_P!o0Q{TiZ4MwP->FpRu=tNIbnTH@r!O7bZ z=cGUcouwVIa?VatB90QyvLez>PSPUs7+Gf#3`#;qO3Fz}Miz_tB}dQ8KLF$9h$YAY zl0|Vq2-X4R;3V$sAR-|yFE1jE6_*u}m&7`WNXkmdJ2+zGrUgOYGWIY`UOh)9b|VML@Ir6oi#(kLepC*aEtj#82`C|L;}q?4n9wzr=r2D}HH zC&mRU=H`QS;XxAYrJ!b_r_3WMD*m%-;(-Zp2DHFgab8Z|!Tvw-7C29=c>soBI|*4C zDH$0QP*qA+TvA%@CxsQ(&mVXUK^QS2iRvq8_+c>t-hLL|-X6+4zXSG1n-TD8CrkiF z6BB?1Y`^2z;#d5Nh)XMo1I@r_6^y){aL%FsPo#u6BECFboImI`^k+<%V|{-@9ylb? zBNZ@?1pij%@y7&Vop^pC9bGYAE?5w$z*~Nh;r{03$zvo?;<8RC5eIpcq=>YfysQWY z>x31Nb&z)w7k9*nOUp|A;`jG<4hY8hVbxrKCjkopKO&|qq~K5c3jLxCcEu8m2dp3h zyg?H6(+V3yYK0;#KpMv%mImiSA@ONTSLH@V=*8=~wz<(|9Ukm)# z0{^wZ|NkuT$JPYv1y(u1VB@hj1FzN7&^T>kW~{Aes0DU;5CkvwaP;&?pdrZ9E5Of8 zM;&?G#umA65!wf_Lky4{B#LqL_c1cpwjgYX{=RIzB<|IqK@ozizo+>}EVUEZf`H8z z5>TKWef$CdY!6^maDWd1&IT|OSgbh#_!@wP{eXf19wp!%e!$BF*o6oaHk=TXpM{wQ z7#m@yiFElB?C>Yp(bdlr&`1LsJ||BvARm7G2kba4 z`60qP05qWdVOxJ^DM=ykoeK zVhEDk1o)IcaC}7&M0*Z`8aw{LIb=alQ#=H*j`?8xFvNaHz#Z-kw*DV0A&A-rf*4;w z5ao#4cXiaM^SveC=Q|r?XF0L*rM!umw zP9Z0;E{+5tFnR_CR%TW)c6KpGbvbp%|JP;j5kyZ8^CE>vVDu229!5eB+v@-!0%C}S z5J$fYj08anCj)HQ^;H5JObrqOryz92bnp~-3KRY( z0-o|)COoAW|GV-Z04cW`M2ZAxQiKo;jTE8;W@e&;b>0BgG=-i^nQnLp8mP-~ zaj?tLF3)k~#1**CS@|E2*OsZVcpfmrs_EGqW^TwS@tJB)$xh`hPH<;eM z_wF9Zj45!g8p}M4sjTzPIjv z-lv@cwlMnglXn%z?u}fy_V(iN?AUmgQth<6;>Q^Yn~C1$Ecs*kt(?d5?|!^^P(CQA zyiWu{)|T(CD1R3gV%yts&*O2>Cxdbq(cZ~c16lsDeWJbR+St&4QSYz*tNG*_W{UrO zcftG<{pN-~vM=*b496Dd-_d-TZQ+`)bF6GBCTq*S_)I7Ljhlx-t$)pe30Vf z$GYkkq^f-zihS?1hAXzTF!=-)JoC-Iw(`!_%3pZFps%hiZ$-N;|Ad0shi+Zhv1%`l zJV0jQE0dr4<@OT?S2vqybq!)HJyrg62g!zh_~BnP{Z-+fC0NkJDX-_^PHwEs9p6v> zxB8k|uF}fN$#(aokIzkAjmyZtt01|medMi8i>P^lOQdX6)j(F5oua%|1X@^CX6{VyjxjwC%#>Q2}#n zu7FnBujW+ma%;F`_i$2Ruk0*ym@Gf^S0nyVl$c;(Gw)7R(xoYN zw?S~urrgEZF2{9WOpbFwO2ykYxed1w(LU%^R+X?ki@S|c%*oaH&+;gTD|f|?$%W-O z))CgBKME4$ieH7OFG*)_(W8BX zhv4Lv+zysd?(mbkyLtK`%XH)eXAf6kibe7`Di@~Hm9mE^iy(~mn-8QEI~p4-us6DM z%4>nnK%aOfu$V#rNEJWKq>d*yW1^Q>k_+zQKRLRTrXbjU?|4Jf=cUz?3QsON*kF4u zpGS^4q)5qVv?(C)h zSaEWr`(kD^WCE5Y(cD@TOknxTj*)eDA!yL3^Fd)!^}h5qtZ9_gRXF=g z>hl}LA>1h>$9kRxnqwk}R5Err8c>tn9}+A-RUt~kN^*#ejOizCU%jZi^uW1&SH zBs9H&M>h|0Vw16yLbQjADMOX%=*kfYWiAuT$`rF`lCE$iu8}U{BH&-;6B>zc7kC+h z^sl5a%D>is)_yv}ueP7gPJH{naKz`|<-xxY{GYD?T?mB`G5;6JKMe5igZcl0KL1~J z0k$r{RX}R}Vf{a=LCE%hDMaTX7>>ADCI=pd2bmJ=7o#YDY`}=yHNp-NWNLuMgY6c` z{KQoFr`;-$i4ZKt7vujT{C`j(0lR4tItUIUA%T;@VWdB{+h8*d(K8_Eq!=ZbklZ|y z_Tp$>X*CU>S`)CxCI>rj7%6-YitpKjPR-mtlsUU0c?g$z23L@0-(eAUvEg)O)m(hm zT9JO5xmNZvYod`_v6m`WWn$^)2)EgfhNDkRHBuII%y7#`kB#W@GFZo*9vJ%A-u>An zjaRQypjE+qvF)g_fXL@f4$DLBOQJ#4Ze)AV1jg6+qhunbK_4A+`G{ z+mo_yu+QxD*%{@^hX!*0oixox3tsGH?uBSl*sx$8LViN=~YChMUtsKnxvj^k=cxsD%( z4tCRZ88v8(p~ugdi2B=YeO?v*;3KSZog0&P zZnWPUKYh`yc_FCkOTgrvOGlDlD(4N?w{0q=e0*8%)t2-j2+axZJv_J#sy^;tVr(bB}?EcT}sgZr%te2t9xlSidEaP)NTG<;CYhf>oH_9Q_YWh-Iygl#I#F zq<|s{UWR=a(?X^X<5Yb7cG>QF9DM$&N^#lfYUA{&=!ssfrHXWBo!lvd&_LknY?jhY zw6dm0LZzHe7WJZK2K6CYStCJ6sky4SIJDN5lI&9oqoML1q;vEw_1lQFh83-*h6S+| zH@RvR)po@3$mtEcL_xKU3#SrSsjnVf4qtBfDQZUZmSs$=1*AqtZOt^5h};fVt_oj? z-*LJQv;TT{C-m;X)*fUneNNBp9*~PV1eJ z@B5$UUu!zLa{0rs*5is<PIG6 zOr=eTvvt#Nb*`HZmnc{Y$#}pk&9WUn?#SA>iF>kVTFEO?XaWg|ddCyz0n|w(E zmQI0ulB%(s@s_ptJNYDrt+lEvv-?F?=;8>!&f{ED4_Ylg){nZ8m#kBmAFqk&_s8yCwvFG_34-59%ny_WGG!u1VjNR3pm%JWoQx7~}d!TD5SFARX?6sAmM-?vm zniaobsR>J=d3E)wqV4&8_R#2lQ|N_>;DuGKEhOi8GCchHEfaTotErcxf(IS$l9GJi zcN(O9E^ydNIA10CeZ&fWG)717gJiI;l_2JM(5edime`csQv8BT>K^U(16->`{b)rG zjB-?5mO%vdXoB6Ahq=~XaYHy~*Dpow&-*G6o!=?~gkCtGd7OMNvrh5O%)XhzAzKgYqaFL7pSr}! zcuc0tf$YM7d`w7oNq3)q5bm;`nMU>WGHWd||=-1Qp_G^O*g^SRt zY+Bvt-UycSL6h^^oK@53!jr~>bdJ_l#snREKX8`mQvdiAY2bW!^1{Qr{MZxAv3Ki- zW2jSBjH4BJ=yL?}jN5N5I1Y8m+e~&f25TuXD~h4?hi%4e$&cu~G0Np_epO(%+B#l% z7`q*G{)5v^R8fty)B8XUX0eoZRB)V(TL@=X;XW6)Gjya%o}h<{L79u-v+)sEE7zir%QQ<+Hi-`TDIlJ5Ht9 z+Z-+YY5ml6Ju)6%N%BEMJGlu`aoE!L-4FT;a^fQL(o9~a^1fx%*>K*yOkNo^7CIdi zWNF?ofB*gKSlPm@PwiB5UDNZAxNWWGE$3U0YDi>6SbDpiIpl0Pf9mS2S z&$R`+ong~`bn?_&M<@OTEY}dmH{3L~{kC{>+to*RSD&q{B}FDxJ}DQAT$ATMb3*lb zT)1`ViuQp$h;_F7R*P6v>Ibu#xkrZsj7g%55z5!ePj^_KdC`2^{_>3riPOxPmBj4lh(o`6P@Ozk(RAY0ivC4_Z%8fQ9*Icu8bKMwqL~NM8r)k-I zzQ7SVdX}8~%@ysZg-M-PHVz);YWv<3an1vxCm9O$1BF)oR$XQoA7(^czkScL;)^!(?VAmav*RDJ z2RSMPdu}H&IDb8jdelgjt?iwMJt5fn@nW-t$5D^#vEO5E62p~{M8F!Cgaq*;TwxGB zw*(ysRV3z$ebi-f^sR^eYwIRHwQqi{a8*Y544>d^uDW<*sQ^x|%mU>rB_<6pUmz{=K44>CEB5 zv+v#u9qfPjT6&@A4R-K)KuCCAt^t1#{(ha#)7``M?ei9rucMUjcbU3uIq)8vv5hqO z?xn)GcrBwnL~U2#68wprwaWI~LF=rLmXZBbMF|6_fNxqIqc7+=Oh+#SeknMI(r2OC zc5TvUD^GH{6B5aIKYHes%HgmtIa%?SKRV8NDjhnnEuHnmRCv3uW1S_WM0W5(Z)O|f z=BYVOv!S-3`}I1tWv3`yR*f$x6`rvT7*BB0M|I%U689-gUKer*d<~;d|cz zn_|{m25NGFB$~V$z2_ND3^_6*DH13#7Fw4V6gm~Z;)yt#@Wd+0P`=*UA3l$jVhsVSw3{sq2z|NTlznSOX$&3xt3SQ=q z{D86Y%H)Ax%yu3$R9ds8lq?)+4GW!}ujV)qekf^6m9?|JRcB3Ao8oGo->z@h10SDOvi=^YIr|9i=yy$5j!>tKu^idw zcV@9fA?eFKe1G?MvNN->j$_?Tui|B!4!-q0jx;&Q9rFs0_l%>}$UR`L#96o%M%ozG zw?-$rB^K|PS{ocgi;1pSnwql=Z?zo!=9q*k92WbYx?7YGxp^sWWWyx)xlxOCNKECG z*Yr9sxJ-lIRbF`->T5PsfGv8RATpiQ?V+_nYCNyu6v*~50#^D&%sS!IB}9ON(o(4JJso(wnWV^NU0Ne!moS`tP-&`}4ij$G*pueks~Jh#4rzneX1pHAqS|mZ z1666=19b!PNri}~hp5m8xyM3_x4)PhR5X9SdNrcBWw(ssVY0{;34)?Qv^V@@>&#WRy zOAnJB?_%bV(Rn^!yjC7Iw`j(q`C;e$`2lOFd4Epidv1L3^c~}O2P))9U3u!mIj2oE z1@^Pg5TsBni_n!+~41D)s;_~>>Yks-v z6Jpn-slbX_`ohXWX%o}RLZz4Mir|Wh&LJ48W<}clVGpgIju+kcw{X7g3f7||Wrw78 zIR@gB^=amc#&S*8!t5S@HDV3n*H;WX@OnBIx#k~p>0Dbta*3b7XC`0LDdqh#4{i!x zEV=W9XMm#h`%-XPGJf;UrRk$;9+;6ckqcA2s*Zpg!}h^tHR8IHtnF zJliJbusHF9UH2CKl`Y?2@u#%Z;uu>&g&6HhmQsEoQ|T~O;*m*HpzyC#-_fkyq^fmu z`oMLHZEePF@uWv`qbO35IV@{fIDPW^)Dg=mO5XZOb!11k!()~kH5s&K_@_&ULoJ5~ zC{nJNZni(n_wYE!HTh^Wh9UDhRoD60+6j*yUar(WzYC&gImCPon@?sjaWp<;blp(GxNvD_Cb2tGRfO>)SDI-(Mu& zGS)V8XO(!VeBmHlq=}y0wA@FIZj$1}F}Y3hM&B3oDb+iY+im*?wt4p(EnGTkF*AF6 z!%>?Qveed&srU9t;{3dwSl9MYt)`c1T9Hn~{9}RCM*QQfpqP0kyP!)kKD;GTq9b{n zeLW2EZki^Ztm5g9kJQLz_SzlnPOV!_Z&bJ_K7)HGLG0F9MpgN!j=)wBlxtG&xkZNa=GvGJ!^1 z#7}cx(D3_a|EfDKF*)zSS0}HLXR!6BQtq5-<5%62PIP>toY8dcGH#^)6z|txbeDFn z@Vmj7*FuE-IT5Sc5ueXLX?!b z+;~b#9(d}rl6~V46O-12*E)n^!&Al`&xr0}m8dcz-F~MprODwM2MHd5%xqwSY+HCz z)(h9g0J&ofdVjiIzb{PBFR{9V)4mycaR6ds7An5?NneW2oFNt-$iK*XYLI26TiOva zDKS@zn8&L)Ps5|nXOy2o!=sBy(Y^C{iQf4XB_DP;41K1D70tA-4=G5S4Vjoq$2?9y z^U#Nuet*gncJ5r?$G7N)V@~2tN|5PU47F^{j2IOj8$`r8M;(G z=~P`riXIn(h`#7N$iPu9CFfat30s$xRLJVq7 zfSrK=|7d~$0i?&50s(pg4TCNOS^+YeKmh3ZQYrXS)c7DZ1jAqZN+Cx80*L$vFCXAd zK|`tJ1YW|W3qgPv2th-53O)t`67jR?7YxDpzzG#Vf>y#nh%jJbKoclaD1c0YdVhNX znSTh>DkIV$ep3RA{f7SqxczTr!vDxf==y(pMgNZ_{LgjxB>@SP`R!4^kbgTs>3?ng z#X_`C8o_^lVg3z3rVw5jC|tR9)bQ5CGtR zcqw;^%})iQDS$%~dI11|fItJ$kY=ZlBY}iv5DroI;S@-S?A~t&{vBBaFD2)L!}y|M zNDwCAh&lzL7q}=&IIE_BYvK9u!g}EMbP&uN{0=0A94ZDfAtd;ck64h=1|=#4ypVr1X6;N!v!vPv+f5&mpYm)VS9P26wOF z59nWf@ZS*lHiLjK;Sx}SM0{8**!jTYd~d@b!h+6l4@&L)PBNc=;W{O7GDsI< zKtpge1c3`ef)DuqgWg_2peLP%<6|IXYa|_n(1ldTz!ZF9tP-MV9M=;a?a8b&0-W^|fYY;a!l-c^ zCq1r*N(BIh58Tt-3C_;|WT}BsqtecJ-NS9+DH_vosBsV`dVFB+An+R11Kt5J0TFoX zL6?Ndh?pnZ4N=1R3=B&Lx1UnGX+=8$DktC0~giH(v@A4dcTn@I~vQAzi+6 z`g)K)x&)+DT_nhKfF(s=6Ua-|MWpHp5`ZW^yd6Qi5?EQPE|P#rAXLE&4la=37=Rp5 zbP=hb7Xp&7(g1J@XaWR!NCq?#0rDG+kFYYKfIuXqLJ(+T0M4n4z{3#0PJlQCM!-NY zX#l5!OBYE8A@FF3fsh*!46YCZc>XTnfP?@eAuwDJV7fpc$fXE8F~)&>LZky;aEBle z$3F{ERLYN(3&yJp)F$u}bb}B-Xh7>vUV#5|`Nd1X{clZwCi`cYAog#|-%7(1^gs}k z&tI8f$iLQ!dIQ-og5D{_E6)>OWRJ?j35-GrLQ+5@z$z_;Iz1If&Y;Ez-ViX>glE|V zh-9O(iBKqox@-c+N4!uVDWrhR;@JrIY>aU#CIQLCGZWnNaD=7_{>%v+%D|p)91)#r zbX#8!HDM2R3nEEy4ElO}C+v~n^oj=@fs+sp!GHf68mLB(ln}S)Myr|n{5%9F9M;qA zK}mOduE!V;VrIwc+|OL+s&A2c>n@;qB|&OY{G-#e^w2{mjUHvM?w@gO&0%Y(bMDDw z@fZhpl&5&ogiT}rLjfyLp}sbD>kVk!B&C_=x}NMIFR=<+dN9?KXAE~T=;95PJpV}A ze821b^Bl?ZFAXnVcj{7>QFk0Nm-L&W&y(8h(ATKC?-TqmP1x~4Wej=$gCyaLEd9?) zMzvYi#_C)*8nQQ;UTCk}@6`E(?oh}{FXIx)3)Ku^TEno*(Z@>nd(x8TVQZ5_-lPfB z>W%I*wI$=7WQ=)vi383O-@Z;m?J-ub6J<!Ewo z+|~;AIz}Z%C&uA(-Fp`@u1k5+?*&)S8GX%b{ON>xQJ`WV}@dgIV{(QgmKat=RCk5qk<44)3H5>hW7W12LILnuP>TgTmo%x>Qk%s3VB(W|5mGhgELe3A%Vq^z+$0^5Zaf-33sDS9sf|9(k+8i56 zr>AKGh9yqV(s)c+4Qq3-N@SjIpQQ;47)HI!eU>J02CvymQeI9`5KRrd#CKHj1S;FA zC&o~);(Vj|bKo9)K&FR4PmJ;7`Oig}kkJM9!AED0E9yF{C@P|+1QK17gi1Ou$D+)i zRy=*A^h^?~%5qSbax%stgFJeoC7v$FiTAlsX$QuLg;$G1KLqhK6wsl3CQxD>Z%3ZP z3^B$eu_7Uk)Mh$SdXl6{k8+i?nbMHvhDpzgXx}+# zwQ5|Wxg3xTt7CWU2oc6SERABd8t>;ioU(R;YrkW-p}=_mq4-bJCBDiyY(n-p#ZAZ1 z4^9;xQ#LfcJ@4wm^ByF3l-B1-tScQazs9(4Hz)myD%ESJ${hDSXrJRc53&wN<*Pyd zDTQmcS572pEhlLzZZ96Cn;8W_Fi~A$%4k+ zOy7`ObvX;Q(^l#%yI;StMuL|sN(WNxk0a=xflNGuRx_o;_+GxU7Io_(Qrx~D{l>&+ zJ>|#OTHt3Egos}IQhsd;``VJ)wxIInXX`niXvio4_Vt44K9xjR|Di*zp4?qSd(h(t zFnZ3uIRsMpsJd80;`*Y>?C5Gch?khww%N>OD+L!x7h#D^Y%kLv1 zuW6FA>$s8}Ev;)>br0!z*WvpOSAEhd%s%t85k=pjdgdP3*<_6?me-4vInp~VhnK&X zN}FAF5RVG*Xuj%)XrPq_%!lW8oyusLuiD_Ue% zFEc16)}>US*g7pCv>ATw@wZPqEEibwQ!L|hQQG+fEIu7-wALLV2P99>-D#B}#rQa= zD_;B-$o{bU#rL`B-E6e(C4^t~jnhrW_!&xW+grt-mG0}7SI(U8MNdC}zRGzfk`DfY zb1q?QM{#tM$pd%EM0kF`Ti@v5@>2VAlc=$=byB3!<8pg55AWR`#;*JA(rRzp2Pahq zCyk65&7b$MH;sN!#v2-j>2Q}y>0dCf>R4%P$m(FUFp|}T=?+M&T)TNdHDS3wIZ3AA zN>U|HwKc61Zk|MwI-#EW=KHj_p+zM1CC06slG`E?G74tY7teeU9hg|)wBVGC*ms5M zZA7H0T(Rvfzv3tKM=i-J&hfK7Em66SeXI+kdzz;$^Yuf>%wvnD&qo3iJy@Bk7r$xTrO&dJX-VdP{UU;JMf)s|g)c~w<6pM77S zUgeuFV6a$mv$HW$mDCYN$ooBW^O!X=!xxoRj@$SrzHRPR~=yx*vNSHqgerg%K>cuX&{U{S(Rh;z7@Eecujm z%PAk;raXtE=?EKt_~HBg*sCn{hTKiru{8_(@y$sy+fiBpHJ1z1Kl;9KJ7kmg?b028 z7pf=OA-jUw7wFUnhfBZ4U^l#2=1q)}s7yKJcK0C0DV2_G#K^jg)Y$h|%JIwXX5l22 z6T1neV)%E(QKef7A_pK&W@QIoD0~m9Dt)sZHfPvX+5TxtK{``{Zv5M!4=Yqh^(jg^ zPlXL@#i|r=RM4jEk}i|ZsBqUu&>3AD3x0j&{7ih4c|U6yGrgmhs%vJj&S-qFLwKNy zN&M?|@;40_lN!qYXQw_jeEFmj{I#H|FZ-At74x^F2hzqx+!se=W2Q*sLU&m>SQj;? zZf-c&&#||2Y%qEFsnu@n!abXI*nC`JS{}#NNYg{QxW}c#I#2_zB`k2s!S7nPPoLZF z7zTqI|1mh5l~>A(TZixU;op^it(oc{i^a59AwHQ&(IvLW@S!iU=nHz_m(hjjdk(WG{8^Qfl{XC9QC zS3dkI@7OJgn4-h&<>}tvJXf}Nd2#}I+&?!)`fbqP4hT>WfX{HZ9=nRI*HOPlHR3I} z87~uO!4w++j#6>=chKP1gcQawJ;pkWj8a#~h3J}FtuLAtIj4h)Bs!Y8q7uoL-=3{C znSw({R%ns_9;yQ{@So1cF(0M&kDgctQ{aJtJI|ON>+rD*7gfAnTlAX_9Lkx_&SyMz z0dvVAX6M1sVZWK4dvQnI{NUD_oN5J?mB-o#AlH4GjQqXbgVjA(cF=jy*5Ss8oip+D z2EpkPOD?s6Hm+wbFyh7`T=ut0;CQtqLKTZd>WCdk-UJ9S?~oK9Yg@_aaRRE;aE z-0-TDfBnIw(CO2L$%0x{=Q}?z2QGB(&v`!S368t>Adw>OCrd-Q-!bI^_><}Z0UWB+ z*44dMqi5Eu@7!EV6+HQLW;-_^oH}31824uJ?NiGBBcJK3mTF4snKN8opqU^bB4xrk z@{NXQG=6l~qv!fY;bY9VyKa@K?sNGYO>x4n?wI(vl-L|gA$u=QHx5M(EMg_!bk5?` z?uMS`RhHO2v=jEljTNi!c;o)Y_wy>oYn<=3(s37Ju(@bd*<(~fZ28x2+4nU#!8{oj zb-SB|)pmovq791?6a)B%OL!5|OhmF9nHz3vc}4V2xo-L~2PcDuPfYJ>yshjdo*3}L zhpAjxqIV4B7b8T=H}0M@FDy}z+`$b@UrN$sw`v$J?}uKxVxtuTnk~mGxYpV1v{mXa z_=d!x9>$!ZJ{lS*RvmYZ-BzMkm(%Q8rYD_iKGj!)GTSSnC+}qqeE6Jjy(T_iXL5(P zvFqFB&0gpD)~g?U2EAO1c_N#;F;^11t>?M|P`7HmScx!01r=2<0o%T5l@6Y^I6V9KcdL8feMCu}bn%mzXEKV9J{r+3C=seXZ zKljs*jr+?)iY-*ru_tDvE-LsMFyGi8WK~P`v>^7|Muxg^)cMhx>YS5T#s>|Mz{k?MDz`D`#JSCOQ6Sor6>ID3DR8EYbZ?OOJYE*lRrYqFW;eYzua zT@h@J+i%bNHyw&&Wt(9G4@IrHNg{XG(cdiMJHfw{vS zs6pL{IRj&R=waA`agERuA

|RJ@N5w|wiUVwg1v5=d;<6|}YgqPT$o4#4VgjILfc zdeUCP$8T%P>eVfwz-nd>7v>9nNg&c4FD>acDW8ox&t>Ue*_4@@_18pppnS3w%` z#T=W?Fwka2b3~fj&?4!WdY*4bc>EM?Ol$E#%o8CF-Nd#Fn+dPUTK!i$dD39v&)4-0 zKA09T-!zlBl8dI|y0A{$)2^~V!A3@CiA{~2#K!KhvCzlx+Ba)w-wN`wDf6!ch*00b zNTlB+q0gP=OdT2~S?HnV6lswBvLtA1Qu&qQG@CodZ8z}iBNVb8vuYG~uB=g2QbHjq zG>2S|{R+9CN3*bsB)T3R{%#!iMgJ3rOi-NI`2L(QVT>a*-u`<1&{gSz)C(M|j=+QXa(EU!B&wng#ifzr5#FW}4m^FnbwgWeFO1zw@&#dNY7JWkt&H3@kGI68_Lv}O6EV4(f8V>%Lo7ut* zQyBPTZ3{CkRg}#wuF~$3web|bhEzvt7;4(X;^N#$Y))$6tHwkyIZeHp=m%tKw)z*& zI*VOf57464r?TZJD}_e!SAwafNMYJtcA9YHCHjoc2h57Mw2$Z>q!TYdzcj-f6uyHe zM+7;zJ6=Cp?8=*&m|n6bDt`0sxl+cc2-nW(lL+Js%7jFL#M2sA(_*`#+A>Iv1PE1X zdU~0iWt5l^NPkgLK&NTI!B { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('active')); + contents.forEach(c => c.classList.remove('active')); + tab.classList.add('active'); + const id = 'tab-' + tab.dataset.tab; + const el = document.getElementById(id); + if (el) el.classList.add('active'); + }); + }); + } + + // Detección de hooks vía AJAX + const detectBtn = document.getElementById('sc-detect-hooks'); + if (detectBtn) { + detectBtn.addEventListener('click', function() { + const phpCode = document.getElementById('php_code') ? document.getElementById('php_code').value : ''; + const nonce = document.getElementById('sc-detect-nonce') ? document.getElementById('sc-detect-nonce').value : ''; + const msgEl = document.getElementById('sc-detect-hooks-message'); + if (msgEl) msgEl.textContent = 'Detectando...'; + + const ajaxUrl = (window.simply_code_ajax && window.simply_code_ajax.ajax_url) ? window.simply_code_ajax.ajax_url : (typeof ajaxurl !== 'undefined' ? ajaxurl : '/'); + + const form = new FormData(); + form.append('action', 'simply_code_detect_hooks'); + form.append('nonce', nonce); + form.append('php_code', phpCode); + + fetch(ajaxUrl, { + method: 'POST', + credentials: 'same-origin', + body: form + }).then(function(resp) { + return resp.json(); + }).then(function(data) { + if (!data) { + if (msgEl) msgEl.textContent = 'Respuesta inválida'; + return; + } + if (data.success) { + const hooks = data.data.hooks || {}; + const critical = data.data.critical_hooks || {}; + const container = document.getElementById('sc-hooks-list'); + if (container) { + container.innerHTML = ''; + if (Object.keys(hooks).length === 0) { + container.innerHTML = '

No se detectaron hooks.

'; + } else { + for (const h in hooks) { + const info = hooks[h] || {}; + const row = document.createElement('div'); + row.className = 'sc-hook-row'; + const html = '' + escapeHtml(h) + ' ' + escapeHtml(info.type || '') + '
' + + 'Prioridad: ' + + (critical[h] ? '⚠️ Hook crítico' : ''); + row.innerHTML = html; + container.appendChild(row); + } + } + } + if (msgEl) msgEl.textContent = 'Hooks detectados'; + } else { + if (msgEl) msgEl.textContent = data.data && data.data.message ? data.data.message : 'Error al detectar hooks'; + } + }).catch(function(err) { + if (msgEl) msgEl.textContent = 'Error AJAX'; + console.error(err); + }); + }); + } + + // helpers + function escapeHtml(str) { + if (!str) return ''; + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + function escapeAttr(str) { + return escapeHtml(str).replace(/'/g, '''); + } +}); diff --git a/includes/class-hook-detector.php b/includes/class-hook-detector.php new file mode 100644 index 0000000..9779723 --- /dev/null +++ b/includes/class-hook-detector.php @@ -0,0 +1,67 @@ + $hook_name, + 'type' => $type, + 'priority' => $priority, + 'accepted_args' => $accepted_args, + 'auto_detected' => true + ]; + } + } + + return $hooks; + } + + /** + * Obtener hooks críticos que necesitan prioridades específicas + */ + public static function get_critical_hooks() { + return [ + 'init' => ['default_priority' => 10, 'description' => 'Inicialización temprana'], + 'wp_loaded' => ['default_priority' => 10, 'description' => 'WordPress completamente cargado'], + 'template_redirect' => ['default_priority' => 10, 'description' => 'Antes de cargar template'], + 'wp_head' => ['default_priority' => 10, 'description' => 'En el head del HTML'], + 'wp_footer' => ['default_priority' => 10, 'description' => 'En el footer del HTML'], + 'wp_enqueue_scripts' => ['default_priority' => 10, 'description' => 'Encolar scripts frontend'], + 'admin_enqueue_scripts' => ['default_priority' => 10, 'description' => 'Encolar scripts admin'], + 'the_content' => ['default_priority' => 10, 'description' => 'Filtrar contenido'], + 'wp_title' => ['default_priority' => 10, 'description' => 'Filtrar título'] + ]; + } + + /** + * Calcular prioridad de carga del snippet + */ + public static function calculate_load_priority($hooks) { + if (empty($hooks)) return 10; + + $critical_hooks = ['init', 'after_setup_theme', 'wp_loaded']; + $min_priority = 999; + + foreach ($hooks as $hook_data) { + if (in_array($hook_data['name'], $critical_hooks)) { + $min_priority = min($min_priority, $hook_data['priority']); + } + } + + return $min_priority === 999 ? 10 : $min_priority; + } +} diff --git a/includes/class-snippet-manager.php b/includes/class-snippet-manager.php new file mode 100644 index 0000000..c05f302 --- /dev/null +++ b/includes/class-snippet-manager.php @@ -0,0 +1,495 @@ +query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_simply_code_%'" ); + $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_simply_code_%'" ); + + // Clear filesystem cache + clearstatcache(); + + // Clear opcache if available + if (function_exists('opcache_reset')) { + @opcache_reset(); + } + } + + /** + * Save snippet with hooks priority support + */ + public static function save_snippet($name, $php, $js, $css, $description = '', $active = true, $hook_priorities = []) { + try { + // Crear directorios si no existen + $directories = [ + rtrim(SC_STORAGE, '/\\') . '/snippets/', + rtrim(SC_STORAGE, '/\\') . '/js/', + rtrim(SC_STORAGE, '/\\') . '/css/', + rtrim(SC_STORAGE, '/\\') . '/backups/' + ]; + foreach ($directories as $dir) { + if (!is_dir($dir)) { + if (!mkdir($dir, 0755, true) && !is_dir($dir)) { + error_log("Simply Code: Failed to create directory: {$dir}"); + return false; + } + } + } + + // Detectar hooks automáticamente (si existe la clase detector) + $detected_hooks = []; + if (class_exists('Simply_Hook_Detector') && method_exists('Simply_Hook_Detector', 'detect_hooks')) { + $detected_hooks = Simply_Hook_Detector::detect_hooks($php); + } + + // Combinar con prioridades configuradas manualmente + $final_hooks = []; + foreach ($detected_hooks as $hook) { + $hook_name = $hook['name']; + $final_hooks[$hook_name] = [ + 'type' => $hook['type'], + 'priority' => isset($hook_priorities[$hook_name]) ? (int)$hook_priorities[$hook_name] : ($hook['priority'] ?? 10), + 'accepted_args' => $hook['accepted_args'] ?? 1, + 'auto_detected' => true + ]; + } + + // Agregar hooks configurados manualmente que no fueron detectados + foreach ($hook_priorities as $hook_name => $priority) { + if (!isset($final_hooks[$hook_name])) { + $final_hooks[$hook_name] = [ + 'type' => 'manual', + 'priority' => (int)$priority, + 'accepted_args' => 1, + 'auto_detected' => false + ]; + } + } + + // Crear metadatos completos + $metadata = [ + 'description' => $description, + 'last_updated' => date('Y-m-d H:i:s'), + 'active' => (bool) $active, + 'hooks' => $final_hooks, + 'load_priority' => (class_exists('Simply_Hook_Detector') && method_exists('Simply_Hook_Detector', 'calculate_load_priority')) + ? Simply_Hook_Detector::calculate_load_priority($final_hooks) + : 10 + ]; + + // Crear backup si existe el archivo original + $php_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.php"; + $backup_dir = rtrim(SC_STORAGE, '/\\') . "/backups/"; + if (file_exists($php_file)) { + $backup_file = $backup_dir . $name . '.php.' . time(); + if (!@copy($php_file, $backup_file)) { + error_log("Simply Code: Failed to create backup for {$name}"); + } + } + + // Guardar todos los archivos con verificación de errores + $files_to_save = [ + rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.php" => $php, + rtrim(SC_STORAGE, '/\\') . "/js/{$name}.js" => $js, + rtrim(SC_STORAGE, '/\\') . "/css/{$name}.css" => $css, + rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.json" => json_encode($metadata, JSON_PRETTY_PRINT) + ]; + + $all_success = true; + foreach ($files_to_save as $file_path => $content) { + $dir_of_file = dirname($file_path); + if (!is_dir($dir_of_file)) { + if (!mkdir($dir_of_file, 0755, true) && !is_dir($dir_of_file)) { + error_log("Simply Code: Failed to create directory for file: {$dir_of_file}"); + $all_success = false; + continue; + } + } + $result = @file_put_contents($file_path, $content, LOCK_EX); + if ($result === false) { + error_log("Simply Code: Failed to write file: {$file_path}"); + $all_success = false; + } + } + + if ($all_success) { + self::invalidate_cache(); + return true; + } + + return false; + } catch (Throwable $e) { + error_log("Simply Code: Error saving snippet {$name} - " . $e->getMessage()); + return false; + } + } + + /** + * Load snippets with priority ordering + */ + public static function load_snippets() { + $dir = rtrim(SC_STORAGE, '/\\') . '/snippets/'; + if (!is_dir($dir)) return; + + $all_snippets = self::get_snippets_with_priorities(); + $safe_mode = (get_option(defined('Simply_Code_Admin::OPTION_SAFE_MODE') ? Simply_Code_Admin::OPTION_SAFE_MODE : 'simply_code_safe_mode', 'on') === 'on'); + + foreach ($all_snippets as $snippet_data) { + if (!self::load_single_snippet($snippet_data['name'], $safe_mode)) { + error_log("Simply Code: Failed to load snippet: {$snippet_data['name']}"); + } + } + } + + /** + * Get snippets ordered by priority + */ + private static function get_snippets_with_priorities() { + $dir = rtrim(SC_STORAGE, '/\\') . '/snippets/'; + $snippets = []; + $order = self::get_order(); + + // Recopilar todos los snippets con sus metadatos + foreach (glob($dir . '*.php') as $file) { + $name = basename($file, '.php'); + $json_file = $dir . $name . '.json'; + $metadata = []; + if (file_exists($json_file)) { + $metadata = json_decode(file_get_contents($json_file), true) ?: []; + } + if (($metadata['active'] ?? true)) { + $order_index = array_search($name, $order); + $snippets[] = [ + 'name' => $name, + 'load_priority' => $metadata['load_priority'] ?? 10, + 'order_index' => $order_index !== false ? $order_index : 999 + ]; + } + } + + // Ordenar primero por load_priority, luego por order_index + usort($snippets, function($a, $b) { + if ($a['load_priority'] === $b['load_priority']) { + return $a['order_index'] <=> $b['order_index']; + } + return $a['load_priority'] <=> $b['load_priority']; + }); + + return $snippets; + } + + /** + * Load a single snippet with proper error handling + */ + private static function load_single_snippet($basename, $safe_mode = true) { + $php_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$basename}.php"; + $json_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$basename}.json"; + + // Check if snippet is active + if (!self::is_snippet_active($json_file)) { + return false; + } + if (!file_exists($php_file)) { + return false; + } + + try { + if ($safe_mode && class_exists('Simply_Syntax_Checker') && method_exists('Simply_Syntax_Checker', 'validate_php')) { + $snippet_code = file_get_contents($php_file); + $syntax_result = Simply_Syntax_Checker::validate_php($snippet_code); + if (isset($syntax_result['valid']) && !$syntax_result['valid']) { + error_log("Simply Code: Syntax error in {$php_file} - " . ($syntax_result['message'] ?? 'unknown')); + return false; + } + } + + require_once $php_file; + return true; + } catch (Throwable $e) { + error_log("Simply Code: Error loading {$php_file} - " . $e->getMessage()); + return false; + } + } + + /** + * Check if snippet is active + */ + private static function is_snippet_active($json_file) { + if (!file_exists($json_file)) { + return true; // Default to active if no metadata + } + $meta = json_decode(file_get_contents($json_file), true); + return !isset($meta['active']) || $meta['active'] === true; + } + + /** + * Enqueue snippet JS and CSS assets for active snippets + */ + public static function enqueue_snippet_assets() { + $dir_js = rtrim(SC_STORAGE, '/\\') . '/js/'; + $dir_css = rtrim(SC_STORAGE, '/\\') . '/css/'; + $snippets = self::list_snippets(true); + + foreach ($snippets as $snippet) { + if (empty($snippet['active'])) continue; + $name = $snippet['name']; + + // JS + $js_file = $dir_js . $name . '.js'; + if (file_exists($js_file)) { + wp_enqueue_script( + 'simply-snippet-' . $name, + plugins_url('storage/js/' . $name . '.js', SC_PATH . 'simply-code.php'), + ['jquery'], + filemtime($js_file), + true + ); + } + + // CSS + $css_file = $dir_css . $name . '.css'; + if (file_exists($css_file)) { + wp_enqueue_style( + 'simply-snippet-' . $name, + plugins_url('storage/css/' . $name . '.css', SC_PATH . 'simply-code.php'), + [], + filemtime($css_file) + ); + } + } + } + + /** + * Toggle snippet status + */ + public static function toggle_snippet_status($name, $active) { + $json_file = rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.json"; + $snippet_data = []; + + // Clear filesystem cache + clearstatcache(true, $json_file); + + // Get existing data + if (file_exists($json_file)) { + $content = file_get_contents($json_file); + $snippet_data = json_decode($content, true) ?: []; + } + + // Update status + $snippet_data['active'] = (bool) $active; + $snippet_data['last_updated'] = date('Y-m-d H:i:s'); + + // Force immediate write with JSON_PRETTY_PRINT + $success = @file_put_contents($json_file, json_encode($snippet_data, JSON_PRETTY_PRINT), LOCK_EX); + if ($success !== false) { + // Clear cache after writing + clearstatcache(true, $json_file); + + // Verify the content was written correctly + $verify_content = file_get_contents($json_file); + $verify_data = json_decode($verify_content, true); + if ($verify_data === null || !isset($verify_data['active']) || $verify_data['active'] !== (bool) $active) { + error_log("Simply Code: Status toggle verification failed for {$name}"); + return false; + } + self::invalidate_cache(); + return true; + } + + return false; + } + + /** + * List all snippets with improved caching + */ + public static function list_snippets($apply_order = true, $force_reload = false) { + // Si no se fuerza la recarga y hay caché, devolverlo + if (self::$snippets_cache !== null && !$force_reload) { + return self::$snippets_cache; + } + + $snippets = []; + $dir = rtrim(SC_STORAGE, '/\\') . '/snippets/'; + if (!is_dir($dir)) { + self::$snippets_cache = $snippets; + return $snippets; + } + + // Obtener snippets y orden + $order = $apply_order ? self::get_order() : []; + $available_snippets = []; + + foreach (scandir($dir) as $file) { + if (pathinfo($file, PATHINFO_EXTENSION) === 'php') { + $name = basename($file, '.php'); + $json_file = $dir . $name . '.json'; + $meta = []; + if (file_exists($json_file)) { + $content = file_get_contents($json_file); + $meta = json_decode($content, true) ?: []; + } + $available_snippets[$name] = [ + 'name' => $name, + 'description' => $meta['description'] ?? '', + 'active' => $meta['active'] ?? true + ]; + } + } + + // Aplicar orden si se solicita + if ($apply_order && !empty($order)) { + $ordered_snippets = []; + foreach ($order as $name) { + if (isset($available_snippets[$name])) { + $ordered_snippets[] = $available_snippets[$name]; + unset($available_snippets[$name]); + } + } + // Agregar snippets no ordenados al final + $snippets = array_merge($ordered_snippets, array_values($available_snippets)); + } else { + $snippets = array_values($available_snippets); + } + + // Guardar en caché + self::$snippets_cache = $snippets; + return $snippets; + } + + /** + * Update snippets order with safer file write + */ + public static function update_snippets_order($names) { + $order_file = SC_PATH . 'includes/snippets-order.php'; + $order_dir = dirname($order_file); + if (!is_dir($order_dir)) { + if (!mkdir($order_dir, 0755, true) && !is_dir($order_dir)) { + error_log("Simply Code: Failed to create directory for order file: {$order_dir}"); + return false; + } + } + + $content = " $name, + 'php' => file_exists($php_file) ? file_get_contents($php_file) : '', + 'js' => file_exists($js_file) ? file_get_contents($js_file) : '', + 'css' => file_exists($css_file) ? file_get_contents($css_file) : '', + 'description' => $desc, + 'active' => $active + ]; + } + + /** + * Delete snippet with improved cleanup + */ + public static function delete_snippet($name) { + $files_to_delete = [ + rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.php", + rtrim(SC_STORAGE, '/\\') . "/snippets/{$name}.json", + rtrim(SC_STORAGE, '/\\') . "/js/{$name}.js", + rtrim(SC_STORAGE, '/\\') . "/css/{$name}.css" + ]; + + $success = true; + foreach ($files_to_delete as $file) { + if (file_exists($file) && !@unlink($file)) { + $success = false; + error_log("Simply Code: Failed to delete file: {$file}"); + } + } + + if ($success) { + self::invalidate_cache(); + } + + return $success; + } +} diff --git a/includes/class-syntax-checker.php b/includes/class-syntax-checker.php new file mode 100644 index 0000000..8d2664a --- /dev/null +++ b/includes/class-syntax-checker.php @@ -0,0 +1,66 @@ + bool, 'message' => string] + */ + public static function check_php($code) { + // Create a temporary file + $temp_file = tempnam(sys_get_temp_dir(), 'sh_syntax_'); + file_put_contents($temp_file, $code); + + // Check syntax using PHP's built-in linter + $output = []; + $return_var = 0; + exec("php -l " . escapeshellarg($temp_file) . " 2>&1", $output, $return_var); + + // Clean up + unlink($temp_file); + + // Process result + if ($return_var !== 0) { + $error_msg = implode("\n", $output); + // Clean up the error message to remove the temp filename + $error_msg = preg_replace('/in ' . preg_quote($temp_file, '/') . ' on/', 'on', $error_msg); + return [ + 'valid' => false, + 'message' => $error_msg + ]; + } + + return [ + 'valid' => true, + 'message' => 'Syntax is valid' + ]; + } + + /** + * Validate a hook before saving + * + * @param string $php_code The PHP code to validate + * @return array ['valid' => bool, 'message' => string] + */ + public static function validate_php($php_code) { + // Check if PHP code is empty or only whitespace + if (empty(trim($php_code))) { + return [ + 'valid' => true, + 'message' => 'Empty PHP code is valid' + ]; + } + + // If code doesn't start with 'theme-scripts', + 1 => 'back-to-top-button', + 2 => 'pmpro-add-fields-to-signup', + 3 => 'pmpro-dynamic-pricing', + 4 => 'bcc-admin-emails', + 5 => 'pmpro-pricing-tables', + 6 => 'current-year', + 7 => 'simple-accordion', + 8 => 'smtp-settings', + 9 => 'custom-login-logo', + 10 => 'auto-redirect-after-logout', + 11 => 'custom-pmpro-email-sender-name', + 12 => 'pmpro-brl-currency-format', + 13 => 'pmpro-hide-toolbar', +); diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..e63f715 --- /dev/null +++ b/readme.txt @@ -0,0 +1,33 @@ +=== Simply Code === +Contributors: DavidCamejo +Tags: snippets, code, modular, functions.php, admin, developer +Requires at least: 5.6 +Tested up to: 6.5 +Requires PHP: 7.4 +Stable tag: 3.0 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Gestión modular de código personalizado como mini-plugins. La alternativa moderna a functions.php. + +== Descripción == +Simply Code es un plugin minimalista para WordPress que permite gestionar snippets de código como módulos independientes, sin depender de la base de datos. Organiza, activa/desactiva y edita tus fragmentos de PHP, JS y CSS desde una interfaz profesional y segura. + +== Instalación == +1. Sube la carpeta `simply-code` al directorio `/wp-content/plugins/`. +2. Activa el plugin desde el menú 'Plugins' de WordPress. +3. Accede al menú "Simply Code" para comenzar a gestionar tus snippets. + +== Características principales == +- Alternativa moderna a functions.php +- Activación/desactivación instantánea de snippets +- Sin dependencia de base de datos +- Validación de sintaxis antes de guardar +- Soporte para PHP, JavaScript y CSS +- Compatible con WordPress Multisite + +== Changelog == +Ver archivo CHANGELOG.md para detalles de cambios. + +== Créditos == +Desarrollado por David Camejo & AI. diff --git a/simply-code.php b/simply-code.php new file mode 100644 index 0000000..de37e27 --- /dev/null +++ b/simply-code.php @@ -0,0 +1,39 @@ +") !important; + background-repeat: no-repeat !important; + background-position: right 15px center !important; + background-size: 12px !important; +} + +/* Selectores con máxima especificidad */ +html body .pmpro_checkout-field select, +html body .pmpro_billing-field select, +html body .pmpro_account-field select, +html body #num_users, +html body #storage_space, +html body #payment_frequency, +body .pmpro_checkout-field select#num_users, +body .pmpro_billing-field select#num_users, +body .pmpro_account-field select#num_users { + -webkit-appearance: none !important; + -moz-appearance: none !important; + -ms-appearance: none !important; + appearance: none !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; + background-repeat: no-repeat !important; + background-position: right 15px center !important; + background-size: 12px !important; + background-color: #cadbe6 !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + border: 1px solid #777 !important; +} + +/* Estados de focus */ +#storage_space:focus, +#num_users:focus, +#payment_frequency:focus { + border-color: #2d9cdb !important; + outline: none !important; + box-shadow: 0 0 0 2px rgba(45, 156, 219, 0.1) !important; +} + +/* ==================================================================== + OPCIONES DEL SELECT + ==================================================================== */ + +#num_users option, +#storage_space option, +#payment_frequency option { + padding: 10px 15px !important; + white-space: normal !important; + overflow: visible !important; + min-height: 40px !important; + line-height: 1.4 !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; + -webkit-text-size-adjust: none !important; + -moz-text-size-adjust: none !important; + -ms-text-size-adjust: none !important; + text-size-adjust: none !important; +} + +/* Opciones deshabilitadas */ +#storage_space option:disabled, +#num_users option:disabled, +#payment_frequency option:disabled { + color: #999 !important; + background-color: #f5f5f5 !important; +} + +/* ==================================================================== + CAMPO DE SUITE OFIMÁTICA CON BORDE + ==================================================================== */ + +.pmpro_checkout-field-office-suite.bordered-field { + border: 1px solid #2d9cdb !important; + border-radius: 8px !important; + padding: 16px !important; + background-color: #f9fbff !important; + margin: 20px 0 !important; + position: relative !important; + overflow-x: hidden !important; + max-width: 100% !important; + box-sizing: border-box !important; +} + +.pmpro_checkout-field-office-suite.bordered-field label { + font-weight: 600 !important; + color: #2d5a87 !important; + display: block !important; + position: relative !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; + max-width: 100% !important; + box-sizing: border-box !important; +} + +/* ==================================================================== + SISTEMA DE TOOLTIPS + ==================================================================== */ + +.pmpro-tooltip-trigger { + color: #2d9cdb !important; + cursor: help !important; + font-size: 22px !important; + vertical-align: top !important; + display: inline-block !important; + width: 20px !important; + height: 20px !important; + transition: all 0.2s ease !important; + position: relative !important; + z-index: 1 !important; +} + +.pmpro-tooltip-trigger:hover { + color: #1e6091 !important; + transform: scale(1.1) !important; +} + +.pmpro-tooltip { + position: absolute !important; + background-color: #333 !important; + color: white !important; + padding: 16px 20px !important; + border-radius: 8px !important; + font-size: 13px !important; + line-height: 1.1 !important; + width: 400px !important; + max-width: 95vw !important; + z-index: 10001 !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important; + word-wrap: break-word !important; + word-break: normal !important; + overflow-wrap: break-word !important; + white-space: normal !important; + text-align: left !important; + box-sizing: border-box !important; + pointer-events: auto !important; + transition: opacity 0.3s ease, visibility 0.3s ease !important; +} + +.pmpro-tooltip p { + margin: 0 0 12px 0 !important; + padding: 0 !important; +} + +.pmpro-tooltip p:last-child { + margin-bottom: 0 !important; +} + +.pmpro-tooltip strong { + color: #fff !important; + font-weight: 600 !important; + display: inline !important; +} + +/* ==================================================================== + MENSAJES DE ALERTA + ==================================================================== */ + +.pmpro_message { + padding: 12px 16px !important; + margin: 16px 0 !important; + border-radius: 6px !important; + font-size: 14px !important; + max-width: 100% !important; + box-sizing: border-box !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; +} + +.pmpro_message.pmpro_error { + background-color: #fdf2f2 !important; + border: 1px solid #fecaca !important; + color: #dc2626 !important; +} + +/* ==================================================================== + BREAKPOINT TABLET (768px) + ==================================================================== */ + +@media (max-width: 768px) { + /* Contenedor principal más compacto */ + fieldset { + padding: 12px 8px !important; + } + + fieldset legend { + font-size: 15px !important; + max-width: calc(100% - 16px) !important; + padding: 0 8px !important; + } + + .pmpro_checkout-field, + .pmpro_billing-field, + .pmpro_account-field { + margin: 12px 0 !important; + padding: 0 5px !important; + } + + /* Selects responsive */ + #storage_space, + #num_users, + #payment_frequency, + select#storage_space, + select#num_users, + select#payment_frequency { + padding: 10px 35px 10px 12px !important; + font-size: 14px !important; + background-size: 11px !important; + background-position: right 12px center !important; + } + + #num_users option, + #storage_space option, + #payment_frequency option { + font-size: 13px !important; + padding: 8px 12px !important; + min-height: 35px !important; + line-height: 1.3 !important; + } + + /* Campo de precio responsive */ + #total_price_display { + font-size: 18px !important; + padding: 10px !important; + } + + /* Sección bordered-field responsive */ + .pmpro_checkout-field-office-suite.bordered-field { + padding: 12px !important; + margin: 15px 0 !important; + border-radius: 6px !important; + } + + .pmpro_checkout-field-office-suite.bordered-field label { + font-size: 16px !important; + margin-bottom: 8px !important; + } + + /* Tooltips responsive */ + .pmpro-tooltip { + width: 350px !important; + max-width: calc(100vw - 20px) !important; + font-size: 13px !important; + padding: 14px 16px !important; + line-height: 1.5 !important; + } + + .pmpro-tooltip-trigger { + font-size: 18px !important; + width: 18px !important; + height: 18px !important; + margin-left: 6px !important; + } +} + +/* ==================================================================== + BREAKPOINT MÓVIL (480px) + ==================================================================== */ + +@media (max-width: 480px) { + /* Reset completo para móviles */ + body { + overflow-x: hidden !important; + } + + /* Contenedores ultra compactos */ + fieldset { + padding: 8px 5px !important; + margin: 0 !important; + } + + fieldset legend { + font-size: 14px !important; + max-width: calc(100vw - 30px) !important; + padding: 0 5px !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + } + + .pmpro_checkout-field, + .pmpro_billing-field, + .pmpro_account-field { + margin: 8px 0 !important; + padding: 0 2px !important; + max-width: 100% !important; + overflow-x: hidden !important; + } + + /* Selects móviles */ + #storage_space, + #num_users, + #payment_frequency, + select#storage_space, + select#num_users, + select#payment_frequency { + padding: 8px 30px 8px 10px !important; + font-size: 13px !important; + background-size: 10px !important; + background-position: right 10px center !important; + min-height: 40px !important; + } + + #num_users option, + #storage_space option, + #payment_frequency option { + font-size: 12px !important; + padding: 6px 10px !important; + min-height: 32px !important; + line-height: 1.3 !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + } + + /* Campo de precio móvil */ + #total_price_display { + font-size: 16px !important; + padding: 8px !important; + text-align: center !important; + } + + /* Sección bordered-field móvil */ + .pmpro_checkout-field-office-suite.bordered-field { + padding: 8px !important; + margin: 10px 0 !important; + border-radius: 4px !important; + overflow-x: hidden !important; + max-width: 100% !important; + } + + .pmpro_checkout-field-office-suite.bordered-field label { + font-size: 16px !important; + margin-bottom: 6px !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + } + + /* Tooltips móviles */ + .pmpro-tooltip { + width: 300px !important; + max-width: calc(100vw - 10px) !important; + font-size: 12px !important; + padding: 12px 14px !important; + line-height: 1.4 !important; + left: 5px !important; + right: 5px !important; + } + + .pmpro-tooltip-trigger { + font-size: 16px !important; + width: 16px !important; + height: 16px !important; + margin-left: 4px !important; + } + + /* Labels más compactos */ + .pmpro_checkout-field label, + .pmpro_billing-field label, + .pmpro_account-field label { + font-size: 13px !important; + line-height: 1.3 !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; + max-width: 100% !important; + margin-bottom: 6px !important; + } + + /* Mensajes de alerta móviles */ + .pmpro_message { + padding: 8px 10px !important; + margin: 10px 0 !important; + font-size: 12px !important; + box-sizing: border-box !important; + } + + /* Profile fields móviles */ + .pmpro_account-profile-field { + margin: 10px 0 !important; + padding: 10px !important; + } +} + +/* ==================================================================== + BREAKPOINT PANTALLAS MUY PEQUEÑAS (360px) + ==================================================================== */ + +@media (max-width: 360px) { + /* Ultra compacto para pantallas pequeñas */ + fieldset { + padding: 5px 3px !important; + } + + fieldset legend { + font-size: 13px !important; + max-width: calc(100vw - 20px) !important; + padding: 0 3px !important; + } + + .pmpro_checkout-field, + .pmpro_billing-field, + .pmpro_account-field { + padding: 0 1px !important; + margin: 6px 0 !important; + } + + /* Selects ultra compactos */ + #storage_space, + #num_users, + #payment_frequency, + select#storage_space, + select#num_users, + select#payment_frequency { + padding: 6px 25px 6px 8px !important; + font-size: 12px !important; + background-size: 8px !important; + background-position: right 8px center !important; + min-height: 36px !important; + } + + #num_users option, + #storage_space option, + #payment_frequency option { + font-size: 11px !important; + padding: 4px 8px !important; + min-height: 28px !important; + line-height: 1.2 !important; + } + + /* Campo de precio ultra compacto */ + #total_price_display { + font-size: 14px !important; + padding: 6px !important; + } + + /* Sección bordered-field ultra compacta */ + .pmpro_checkout-field-office-suite.bordered-field { + padding: 6px !important; + margin: 8px 0 !important; + } + + .pmpro_checkout-field-office-suite.bordered-field label { + font-size: 16px !important; + margin-bottom: 4px !important; + } + + /* Tooltips ultra pequeños */ + .pmpro-tooltip { + width: 280px !important; + max-width: calc(100vw - 5px) !important; + font-size: 11px !important; + padding: 10px 12px !important; + left: 2px !important; + right: 2px !important; + } + + .pmpro-tooltip-trigger { + font-size: 14px !important; + width: 14px !important; + height: 14px !important; + } + + /* Labels ultra compactos */ + .pmpro_checkout-field label, + .pmpro_billing-field label, + .pmpro_account-field label { + font-size: 12px !important; + line-height: 1.2 !important; + margin-bottom: 4px !important; + } +} + +/* ==================================================================== + PREVENIR INTERFERENCIAS DE NAVEGADORES + ==================================================================== */ + +/* Webkit */ +select#num_users::-ms-expand, +select#storage_space::-ms-expand, +select#payment_frequency::-ms-expand { + display: none !important; +} + +/* Internet Explorer */ +select#num_users::-ms-dropdown, +select#storage_space::-ms-dropdown, +select#payment_frequency::-ms-dropdown { + display: none !important; +} + +/* Firefox */ +select#num_users:-moz-focusring, +select#storage_space:-moz-focusring, +select#payment_frequency:-moz-focusring { + color: transparent !important; + text-shadow: 0 0 0 #000 !important; +} + +/* ==================================================================== + ESTADOS ESPECIALES + ==================================================================== */ + +/* Selects disabled */ +#storage_space:disabled, +#num_users:disabled, +#payment_frequency:disabled { + background-color: #f5f5f5 !important; + color: #999 !important; + cursor: not-allowed !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; +} + +/* Estados de validación */ +.pmpro_checkout-field.pmpro_error select { + border-color: #dc2626 !important; + box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1) !important; +} + +.pmpro_checkout-field.pmpro_error select:focus { + border-color: #dc2626 !important; + box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.2) !important; +} + +/* ==================================================================== + COMPATIBILIDAD CON TEMAS Y PAGE BUILDERS + ==================================================================== */ + +/* Astra Theme */ +.ast-container .pmpro_checkout-field, +.ast-container .pmpro_billing-field, +.ast-container .pmpro_account-field { + max-width: 100% !important; + overflow-x: hidden !important; +} + +.ast-container select#num_users, +.ast-container select#storage_space, +.ast-container select#payment_frequency { + width: 100% !important; + max-width: 100% !important; + -webkit-appearance: none !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; +} + +/* Elementor */ +.elementor .pmpro_checkout-field, +.elementor .pmpro_billing-field, +.elementor .pmpro_account-field, +.elementor-widget .pmpro_checkout-field { + max-width: 100% !important; + width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +.elementor select#num_users, +.elementor select#storage_space, +.elementor select#payment_frequency { + width: 100% !important; + max-width: 100% !important; + -webkit-appearance: none !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; +} + +/* GeneratePress */ +.site-content .pmpro_checkout-field, +.entry-content .pmpro_checkout-field, +.main-content .pmpro_checkout-field { + max-width: 100% !important; + overflow-x: hidden !important; +} + +.site-content select#num_users, +.site-content select#storage_space, +.site-content select#payment_frequency { + width: 100% !important; + max-width: 100% !important; + -webkit-appearance: none !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; +} + +/* Divi Theme */ +.et_pb_row .pmpro_checkout-field, +.et_pb_column .pmpro_checkout-field { + max-width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +/* Avada Theme */ +.fusion-builder-row .pmpro_checkout-field, +.fusion-layout-column .pmpro_checkout-field { + max-width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +/* ==================================================================== + LIMPIAR ESTILOS CONFLICTIVOS + ==================================================================== */ + +/* Remover pseudo-elementos que puedan crear flechas adicionales */ +select#num_users::after, +select#num_users::before, +select#storage_space::after, +select#storage_space::before, +select#payment_frequency::after, +select#payment_frequency::before { + display: none !important; + content: none !important; +} + +/* Background consistente */ +select#num_users, +select#storage_space, +select#payment_frequency { + background-attachment: scroll !important; + background-clip: padding-box !important; + background-origin: padding-box !important; +} + +/* ==================================================================== + UTILITIES Y DEBUG + ==================================================================== */ + +/* Clase utility para forzar responsive */ +.pmpro-force-responsive { + max-width: 100% !important; + width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +/* Debug clase (remover en producción) */ +.pmpro-debug-mobile { + outline: 2px solid red !important; +} + +/* Prevenir zoom en iOS */ +@media screen and (-webkit-min-device-pixel-ratio: 0) { + select#num_users, + select#storage_space, + select#payment_frequency { + font-size: 16px !important; + } +} + +@media (max-width: 480px) { + /* Mantener 16px mínimo en iOS para prevenir zoom */ + select#num_users, + select#storage_space, + select#payment_frequency { + font-size: 16px !important; + } +} + +/* ==================================================================== + ACCESIBILIDAD + ==================================================================== */ + +/* Focus visible mejorado */ +#storage_space:focus-visible, +#num_users:focus-visible, +#payment_frequency:focus-visible { + outline: 2px solid #2d9cdb !important; + outline-offset: 2px !important; +} + +/* High contrast mode */ +@media (prefers-contrast: high) { + .pmpro-tooltip { + background-color: #000 !important; + border: 2px solid #fff !important; + } + + .pmpro_checkout-field-office-suite.bordered-field { + border-width: 2px !important; + border-color: #000 !important; + } +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + .pmpro-tooltip-trigger { + transition: none !important; + } + + .pmpro-tooltip { + transition: none !important; + } +} + +/* ==================================================================== + ESTILOS PARA PRORRATEO (UPGRADES) + ==================================================================== */ + +/* Estado de prorrateo en el campo de precio */ +#total_price_display.prorated-price { + background-color: #fff8e1 !important; + border: 2px solid #ffd54f !important; + color: #8c6b08 !important; + font-weight: 700 !important; +} + +/* Label para precio prorrateado */ +.pmpro_checkout-field-price-display.prorated-label label { + color: #8c6b08 !important; + font-weight: 700 !important; +} + +/* Mensaje de información de prorrateo */ +.pmpro-prorated-notice { + background-color: #e8f5e8 !important; + border: 1px solid #4caf50 !important; + border-radius: 6px !important; + padding: 10px 16px !important; + margin: 10px 0 !important; + font-size: 14px !important; + line-height: 1.3 !important; + color: #26692a !important; + max-width: 100% !important; + box-sizing: border-box !important; +} + +.pmpro-prorated-notice p { + margin: 0 0 8px 0 !important; + padding: 0 !important; +} + +.pmpro-prorated-notice p:last-child { + margin-bottom: 0 !important; +} + +/* Icono de información */ +.pmpro-prorated-notice::before { + content: "ℹ️"; + margin-right: 8px; + font-size: 16px; +} + +/* Estilos para precio prorrateado */ +.prorated-price { + color: #b33; /* rojo oscuro para resaltar */ + font-weight: 700; + transition: color 0.15s; +} + +.pmpro-prorated-notice { + border-left: 4px solid #b33; + background: #fff5f5; + padding: 10px; + margin-top: 8px; + border-radius: 4px; + color: #333; + font-size: 0.95em; +} + +.prorated-label { + color: #b33; + font-weight: 600; +} + +/* Responsive para mensajes de prorrateo */ +@media (max-width: 768px) { + .pmpro-prorated-notice { + padding: 10px 12px !important; + font-size: 13px !important; + } +} + +@media (max-width: 480px) { + .pmpro-prorated-notice { + padding: 8px 10px !important; + font-size: 12px !important; + } +} diff --git a/storage/css/pmpro-dynamic-pricing.css b/storage/css/pmpro-dynamic-pricing.css new file mode 100644 index 0000000..0ca10a9 --- /dev/null +++ b/storage/css/pmpro-dynamic-pricing.css @@ -0,0 +1,824 @@ +/* Dynamic pricing for PMPro CSS - VERSIÓN COMPLETA OPTIMIZADA v2.1 */ + +/* ==================================================================== + RESET Y BASE + ==================================================================== */ + +/* Prevenir desbordamiento horizontal global */ +* { + box-sizing: border-box !important; +} + +body { + overflow-x: hidden !important; +} + +/* ==================================================================== + ESTILOS GENERALES DEL FORMULARIO + ==================================================================== */ + +#total_price_display { + background-color: #edf5f9 !important; + border: none !important; + font-size: 20px !important; + font-weight: 600 !important; + cursor: default !important; + pointer-events: none !important; + color: #2d5a87 !important; + text-align: center !important; + max-width: 100% !important; + width: 100% !important; + padding: 12px !important; + border-radius: 6px !important; + box-sizing: border-box !important; +} + +.pmpro_account-profile-field { + margin: 20px 0 !important; + padding: 15px !important; + background: #f8f8f8 !important; + border-radius: 5px !important; + border-left: 4px solid #2d9cdb !important; + max-width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +.pmpro_nextcloud_config_details p { + margin: 8px 0 !important; + line-height: 1.4 !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; +} + +.pmpro_nextcloud_config_details h3 { + color: #2d5a87 !important; + margin-bottom: 15px !important; + border-bottom: 2px solid #e0e0e0 !important; + padding-bottom: 8px !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; +} + +/* ==================================================================== + CONTENEDORES PRINCIPALES - CORRECCIÓN PARA MÓVILES + ==================================================================== */ + +/* Contenedor principal del formulario */ +#pmpro_checkout, +.pmpro_checkout, +.pmpro_billing, +.pmpro_account { + max-width: 100% !important; + overflow-x: hidden !important; + box-sizing: border-box !important; +} + +/* Fieldsets responsive */ +fieldset { + max-width: 100% !important; + box-sizing: border-box !important; + margin: 0 !important; + overflow-x: hidden !important; +} + +fieldset legend { + max-width: calc(100% - 20px) !important; + padding: 0 10px !important; + font-size: 16px !important; + font-weight: 600 !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; + box-sizing: border-box !important; +} + +/* Campos individuales */ +.pmpro_checkout-field, +.pmpro_billing-field, +.pmpro_account-field { + max-width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; + margin: 15px 0 !important; + padding: 0 !important; +} + +/* ==================================================================== + SELECTS - OPTIMIZADOS PARA MÓVILES + ==================================================================== */ + +/* Base para todos los selects con flecha única */ +#storage_space, +#office_suite, +#payment_frequency, +select#office_suite, +select#storage_space, +select#payment_frequency { + width: 100% !important; + max-width: 100% !important; + min-width: 100% !important; + padding: 12px 40px 12px 15px !important; + border: 1px solid #ddd !important; + border-radius: 6px !important; + font-size: 14px !important; + line-height: 1.4 !important; + transition: border-color 0.3s ease !important; + box-sizing: border-box !important; + + /* Remover estilos nativos */ + -webkit-appearance: none !important; + -moz-appearance: none !important; + -ms-appearance: none !important; + appearance: none !important; + + /* Flecha personalizada única */ + background-color: white !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; + background-repeat: no-repeat !important; + background-position: right 15px center !important; + background-size: 12px !important; +} + +/* Selectores con máxima especificidad */ +html body .pmpro_checkout-field select, +html body .pmpro_billing-field select, +html body .pmpro_account-field select, +html body #office_suite, +html body #storage_space, +html body #payment_frequency, +body .pmpro_checkout-field select#office_suite, +body .pmpro_billing-field select#office_suite, +body .pmpro_account-field select#office_suite { + -webkit-appearance: none !important; + -moz-appearance: none !important; + -ms-appearance: none !important; + appearance: none !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; + background-repeat: no-repeat !important; + background-position: right 15px center !important; + background-size: 12px !important; + background-color: #cadbe6 !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + border: 1px solid #777 !important; +} + +/* Estados de focus */ +#storage_space:focus, +#office_suite:focus, +#payment_frequency:focus { + border-color: #2d9cdb !important; + outline: none !important; + box-shadow: 0 0 0 2px rgba(45, 156, 219, 0.1) !important; +} + +/* ==================================================================== + OPCIONES DEL SELECT + ==================================================================== */ + +#office_suite option, +#storage_space option, +#payment_frequency option { + padding: 10px 15px !important; + white-space: normal !important; + overflow: visible !important; + min-height: 40px !important; + line-height: 1.4 !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; + -webkit-text-size-adjust: none !important; + -moz-text-size-adjust: none !important; + -ms-text-size-adjust: none !important; + text-size-adjust: none !important; +} + +/* Opciones deshabilitadas */ +#storage_space option:disabled, +#office_suite option:disabled, +#payment_frequency option:disabled { + color: #999 !important; + background-color: #f5f5f5 !important; +} + +/* ==================================================================== + CAMPO DE SUITE OFIMÁTICA CON BORDE + ==================================================================== */ + +.pmpro_checkout-field-office-suite.bordered-field { + border: 1px solid #2d9cdb !important; + border-radius: 8px !important; + padding: 16px !important; + background-color: #f9fbff !important; + margin: 20px 0 !important; + position: relative !important; + overflow-x: hidden !important; + max-width: 100% !important; + box-sizing: border-box !important; +} + +.pmpro_checkout-field-office-suite.bordered-field label { + font-weight: 600 !important; + color: #2d5a87 !important; + display: block !important; + position: relative !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; + max-width: 100% !important; + box-sizing: border-box !important; +} + +/* ==================================================================== + SISTEMA DE TOOLTIPS + ==================================================================== */ + +.pmpro-tooltip-trigger { + color: #2d9cdb !important; + cursor: help !important; + font-size: 22px !important; + vertical-align: top !important; + display: inline-block !important; + width: 20px !important; + height: 20px !important; + transition: all 0.2s ease !important; + position: relative !important; + z-index: 1 !important; +} + +.pmpro-tooltip-trigger:hover { + color: #1e6091 !important; + transform: scale(1.1) !important; +} + +.pmpro-tooltip { + position: absolute !important; + background-color: #333 !important; + color: white !important; + padding: 16px 20px !important; + border-radius: 8px !important; + font-size: 13px !important; + line-height: 1.1 !important; + width: 400px !important; + max-width: 95vw !important; + z-index: 10001 !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important; + word-wrap: break-word !important; + word-break: normal !important; + overflow-wrap: break-word !important; + white-space: normal !important; + text-align: left !important; + box-sizing: border-box !important; + pointer-events: auto !important; + transition: opacity 0.3s ease, visibility 0.3s ease !important; +} + +.pmpro-tooltip p { + margin: 0 0 12px 0 !important; + padding: 0 !important; +} + +.pmpro-tooltip p:last-child { + margin-bottom: 0 !important; +} + +.pmpro-tooltip strong { + color: #fff !important; + font-weight: 600 !important; + display: inline !important; +} + +/* ==================================================================== + MENSAJES DE ALERTA + ==================================================================== */ + +.pmpro_message { + padding: 12px 16px !important; + margin: 16px 0 !important; + border-radius: 6px !important; + font-size: 14px !important; + max-width: 100% !important; + box-sizing: border-box !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; +} + +.pmpro_message.pmpro_error { + background-color: #fdf2f2 !important; + border: 1px solid #fecaca !important; + color: #dc2626 !important; +} + +/* ==================================================================== + BREAKPOINT TABLET (768px) + ==================================================================== */ + +@media (max-width: 768px) { + /* Contenedor principal más compacto */ + fieldset { + padding: 12px 8px !important; + } + + fieldset legend { + font-size: 15px !important; + max-width: calc(100% - 16px) !important; + padding: 0 8px !important; + } + + .pmpro_checkout-field, + .pmpro_billing-field, + .pmpro_account-field { + margin: 12px 0 !important; + padding: 0 5px !important; + } + + /* Selects responsive */ + #storage_space, + #office_suite, + #payment_frequency, + select#storage_space, + select#office_suite, + select#payment_frequency { + padding: 10px 35px 10px 12px !important; + font-size: 14px !important; + background-size: 11px !important; + background-position: right 12px center !important; + } + + #office_suite option, + #storage_space option, + #payment_frequency option { + font-size: 13px !important; + padding: 8px 12px !important; + min-height: 35px !important; + line-height: 1.3 !important; + } + + /* Campo de precio responsive */ + #total_price_display { + font-size: 18px !important; + padding: 10px !important; + } + + /* Sección bordered-field responsive */ + .pmpro_checkout-field-office-suite.bordered-field { + padding: 12px !important; + margin: 15px 0 !important; + border-radius: 6px !important; + } + + .pmpro_checkout-field-office-suite.bordered-field label { + font-size: 16px !important; + margin-bottom: 8px !important; + } + + /* Tooltips responsive */ + .pmpro-tooltip { + width: 350px !important; + max-width: calc(100vw - 20px) !important; + font-size: 13px !important; + padding: 14px 16px !important; + line-height: 1.5 !important; + } + + .pmpro-tooltip-trigger { + font-size: 18px !important; + width: 18px !important; + height: 18px !important; + margin-left: 6px !important; + } +} + +/* ==================================================================== + BREAKPOINT MÓVIL (480px) + ==================================================================== */ + +@media (max-width: 480px) { + /* Reset completo para móviles */ + body { + overflow-x: hidden !important; + } + + /* Contenedores ultra compactos */ + fieldset { + padding: 8px 5px !important; + margin: 0 !important; + } + + fieldset legend { + font-size: 14px !important; + max-width: calc(100vw - 30px) !important; + padding: 0 5px !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + } + + .pmpro_checkout-field, + .pmpro_billing-field, + .pmpro_account-field { + margin: 8px 0 !important; + padding: 0 2px !important; + max-width: 100% !important; + overflow-x: hidden !important; + } + + /* Selects móviles */ + #storage_space, + #office_suite, + #payment_frequency, + select#storage_space, + select#office_suite, + select#payment_frequency { + padding: 8px 30px 8px 10px !important; + font-size: 13px !important; + background-size: 10px !important; + background-position: right 10px center !important; + min-height: 40px !important; + } + + #office_suite option, + #storage_space option, + #payment_frequency option { + font-size: 12px !important; + padding: 6px 10px !important; + min-height: 32px !important; + line-height: 1.3 !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + } + + /* Campo de precio móvil */ + #total_price_display { + font-size: 16px !important; + padding: 8px !important; + text-align: center !important; + } + + /* Sección bordered-field móvil */ + .pmpro_checkout-field-office-suite.bordered-field { + padding: 8px !important; + margin: 10px 0 !important; + border-radius: 4px !important; + overflow-x: hidden !important; + max-width: 100% !important; + } + + .pmpro_checkout-field-office-suite.bordered-field label { + font-size: 16px !important; + margin-bottom: 6px !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + } + + /* Tooltips móviles */ + .pmpro-tooltip { + width: 300px !important; + max-width: calc(100vw - 10px) !important; + font-size: 12px !important; + padding: 12px 14px !important; + line-height: 1.4 !important; + left: 5px !important; + right: 5px !important; + } + + .pmpro-tooltip-trigger { + font-size: 16px !important; + width: 16px !important; + height: 16px !important; + margin-left: 4px !important; + } + + /* Labels más compactos */ + .pmpro_checkout-field label, + .pmpro_billing-field label, + .pmpro_account-field label { + font-size: 13px !important; + line-height: 1.3 !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; + max-width: 100% !important; + margin-bottom: 6px !important; + } + + /* Mensajes de alerta móviles */ + .pmpro_message { + padding: 8px 10px !important; + margin: 10px 0 !important; + font-size: 12px !important; + box-sizing: border-box !important; + } + + /* Profile fields móviles */ + .pmpro_account-profile-field { + margin: 10px 0 !important; + padding: 10px !important; + } +} + +/* ==================================================================== + BREAKPOINT PANTALLAS MUY PEQUEÑAS (360px) + ==================================================================== */ + +@media (max-width: 360px) { + /* Ultra compacto para pantallas pequeñas */ + fieldset { + padding: 5px 3px !important; + } + + fieldset legend { + font-size: 13px !important; + max-width: calc(100vw - 20px) !important; + padding: 0 3px !important; + } + + .pmpro_checkout-field, + .pmpro_billing-field, + .pmpro_account-field { + padding: 0 1px !important; + margin: 6px 0 !important; + } + + /* Selects ultra compactos */ + #storage_space, + #office_suite, + #payment_frequency, + select#storage_space, + select#office_suite, + select#payment_frequency { + padding: 6px 25px 6px 8px !important; + font-size: 12px !important; + background-size: 8px !important; + background-position: right 8px center !important; + min-height: 36px !important; + } + + #office_suite option, + #storage_space option, + #payment_frequency option { + font-size: 11px !important; + padding: 4px 8px !important; + min-height: 28px !important; + line-height: 1.2 !important; + } + + /* Campo de precio ultra compacto */ + #total_price_display { + font-size: 14px !important; + padding: 6px !important; + } + + /* Sección bordered-field ultra compacta */ + .pmpro_checkout-field-office-suite.bordered-field { + padding: 6px !important; + margin: 8px 0 !important; + } + + .pmpro_checkout-field-office-suite.bordered-field label { + font-size: 16px !important; + margin-bottom: 4px !important; + } + + /* Tooltips ultra pequeños */ + .pmpro-tooltip { + width: 280px !important; + max-width: calc(100vw - 5px) !important; + font-size: 11px !important; + padding: 10px 12px !important; + left: 2px !important; + right: 2px !important; + } + + .pmpro-tooltip-trigger { + font-size: 14px !important; + width: 14px !important; + height: 14px !important; + } + + /* Labels ultra compactos */ + .pmpro_checkout-field label, + .pmpro_billing-field label, + .pmpro_account-field label { + font-size: 12px !important; + line-height: 1.2 !important; + margin-bottom: 4px !important; + } +} + +/* ==================================================================== + PREVENIR INTERFERENCIAS DE NAVEGADORES + ==================================================================== */ + +/* Webkit */ +select#office_suite::-ms-expand, +select#storage_space::-ms-expand, +select#payment_frequency::-ms-expand { + display: none !important; +} + +/* Internet Explorer */ +select#office_suite::-ms-dropdown, +select#storage_space::-ms-dropdown, +select#payment_frequency::-ms-dropdown { + display: none !important; +} + +/* Firefox */ +select#office_suite:-moz-focusring, +select#storage_space:-moz-focusring, +select#payment_frequency:-moz-focusring { + color: transparent !important; + text-shadow: 0 0 0 #000 !important; +} + +/* ==================================================================== + ESTADOS ESPECIALES + ==================================================================== */ + +/* Selects disabled */ +#storage_space:disabled, +#office_suite:disabled, +#payment_frequency:disabled { + background-color: #f5f5f5 !important; + color: #999 !important; + cursor: not-allowed !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; +} + +/* Estados de validación */ +.pmpro_checkout-field.pmpro_error select { + border-color: #dc2626 !important; + box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1) !important; +} + +.pmpro_checkout-field.pmpro_error select:focus { + border-color: #dc2626 !important; + box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.2) !important; +} + +/* ==================================================================== + COMPATIBILIDAD CON TEMAS Y PAGE BUILDERS + ==================================================================== */ + +/* Astra Theme */ +.ast-container .pmpro_checkout-field, +.ast-container .pmpro_billing-field, +.ast-container .pmpro_account-field { + max-width: 100% !important; + overflow-x: hidden !important; +} + +.ast-container select#office_suite, +.ast-container select#storage_space, +.ast-container select#payment_frequency { + width: 100% !important; + max-width: 100% !important; + -webkit-appearance: none !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; +} + +/* Elementor */ +.elementor .pmpro_checkout-field, +.elementor .pmpro_billing-field, +.elementor .pmpro_account-field, +.elementor-widget .pmpro_checkout-field { + max-width: 100% !important; + width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +.elementor select#office_suite, +.elementor select#storage_space, +.elementor select#payment_frequency { + width: 100% !important; + max-width: 100% !important; + -webkit-appearance: none !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; +} + +/* GeneratePress */ +.site-content .pmpro_checkout-field, +.entry-content .pmpro_checkout-field, +.main-content .pmpro_checkout-field { + max-width: 100% !important; + overflow-x: hidden !important; +} + +.site-content select#office_suite, +.site-content select#storage_space, +.site-content select#payment_frequency { + width: 100% !important; + max-width: 100% !important; + -webkit-appearance: none !important; + background-image: url("data:image/svg+xml;charset=US-ASCII,") !important; +} + +/* Divi Theme */ +.et_pb_row .pmpro_checkout-field, +.et_pb_column .pmpro_checkout-field { + max-width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +/* Avada Theme */ +.fusion-builder-row .pmpro_checkout-field, +.fusion-layout-column .pmpro_checkout-field { + max-width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +/* ==================================================================== + LIMPIAR ESTILOS CONFLICTIVOS + ==================================================================== */ + +/* Remover pseudo-elementos que puedan crear flechas adicionales */ +select#office_suite::after, +select#office_suite::before, +select#storage_space::after, +select#storage_space::before, +select#payment_frequency::after, +select#payment_frequency::before { + display: none !important; + content: none !important; +} + +/* Background consistente */ +select#office_suite, +select#storage_space, +select#payment_frequency { + background-attachment: scroll !important; + background-clip: padding-box !important; + background-origin: padding-box !important; +} + +/* ==================================================================== + UTILITIES Y DEBUG + ==================================================================== */ + +/* Clase utility para forzar responsive */ +.pmpro-force-responsive { + max-width: 100% !important; + width: 100% !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +/* Debug clase (remover en producción) */ +.pmpro-debug-mobile { + outline: 2px solid red !important; +} + +/* Prevenir zoom en iOS */ +@media screen and (-webkit-min-device-pixel-ratio: 0) { + select#office_suite, + select#storage_space, + select#payment_frequency { + font-size: 16px !important; + } +} + +@media (max-width: 480px) { + /* Mantener 16px mínimo en iOS para prevenir zoom */ + select#office_suite, + select#storage_space, + select#payment_frequency { + font-size: 16px !important; + } +} + +/* ==================================================================== + ACCESIBILIDAD + ==================================================================== */ + +/* Focus visible mejorado */ +#storage_space:focus-visible, +#office_suite:focus-visible, +#payment_frequency:focus-visible { + outline: 2px solid #2d9cdb !important; + outline-offset: 2px !important; +} + +/* High contrast mode */ +@media (prefers-contrast: high) { + .pmpro-tooltip { + background-color: #000 !important; + border: 2px solid #fff !important; + } + + .pmpro_checkout-field-office-suite.bordered-field { + border-width: 2px !important; + border-color: #000 !important; + } +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + .pmpro-tooltip-trigger { + transition: none !important; + } + + .pmpro-tooltip { + transition: none !important; + } +} diff --git a/storage/css/pmpro-hide-toolbar.css b/storage/css/pmpro-hide-toolbar.css new file mode 100644 index 0000000..e69de29 diff --git a/storage/css/pmpro-pricing-tables.css b/storage/css/pmpro-pricing-tables.css new file mode 100644 index 0000000..e69de29 diff --git a/storage/css/simple-accordion.css b/storage/css/simple-accordion.css new file mode 100644 index 0000000..a926254 --- /dev/null +++ b/storage/css/simple-accordion.css @@ -0,0 +1,127 @@ +.simple-accordion { + border-radius: 0; + overflow: hidden; +} + +.simple-accordion .accordion-item { + border: 1px solid #999; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + margin-bottom: 5px; +} + +.simple-accordion .accordion-item:last-child { + border-bottom: none; +} + +.simple-accordion .accordion-header { + cursor: pointer; + padding: 5px 20px; + background: #e5e5e5; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + transition: all 0.3s ease; + user-select: none; +} + +.simple-accordion .accordion-header.active { + border-bottom: 1px solid #a8a8a8; +} + +.simple-accordion .accordion-header:focus { + outline: none; +} + +.simple-accordion .accordion-header:hover { + background: #d9d9d9; +} + +.simple-accordion .accordion-header.active { + background: #d9d9d9; + color: #000; +} + +.simple-accordion .accordion-header.active:focus { + outline: none; +} + +.simple-accordion .accordion-title-wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.simple-accordion .accordion-icon-left { + font-size: 20px; + width: 20px; + height: 20px; + color: #555; + transition: color 0.3s ease; +} + +.simple-accordion .accordion-header.active .accordion-icon-left { + color: #555; +} + +.simple-accordion .accordion-title { + font-size: 1.2em; + font-weight: 600; + line-height: 1.2; + color: #000; +} + +.simple-accordion .accordion-icon { + font-size: 1.5em; + font-weight: bold; + transition: transform 0.3s ease; + min-width: 30px; + text-align: center; + color: #555; +} + +.simple-accordion .accordion-header.active .accordion-icon { + transform: rotate(45deg); + color: #555; +} + +.simple-accordion .accordion-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s ease-in-out; + background: #f1f1f1; +} + +.simple-accordion .accordion-content.active { + max-height: 5000px; +} + +.simple-accordion .accordion-body { + padding: 2px 12px; + border-top: 1px solid #e0e0e0; +} + +/* Responsive */ +@media (max-width: 768px) { + .simple-accordion .accordion-header { + padding: 5px 20px; + } + + .simple-accordion .accordion-body { + padding: 2px 12px; + } + + .simple-accordion .accordion-title-wrapper { + gap: 10px; + } + + .simple-accordion .accordion-icon-left { + font-size: 20px; + width: 18px; + height: 18px; + } + + .simple-accordion .accordion-title { + font-size: 1em; + } +} diff --git a/storage/css/smtp-settings.css b/storage/css/smtp-settings.css new file mode 100644 index 0000000..e69de29 diff --git a/storage/css/theme-scripts.css b/storage/css/theme-scripts.css new file mode 100644 index 0000000..5f7cb30 --- /dev/null +++ b/storage/css/theme-scripts.css @@ -0,0 +1 @@ +/* Tu código CSS aquí */ diff --git a/storage/js/auto-redirect-after-logout.js b/storage/js/auto-redirect-after-logout.js new file mode 100644 index 0000000..e69de29 diff --git a/storage/js/back-to-top-button.js b/storage/js/back-to-top-button.js new file mode 100644 index 0000000..75b7d7d --- /dev/null +++ b/storage/js/back-to-top-button.js @@ -0,0 +1,14 @@ +// Back to Top Button JS +document.addEventListener("DOMContentLoaded", function(event) { + var offset = 500, duration = 400; + var toTopButton = document.getElementById('toTop'); + + window.addEventListener('scroll', function() { + (window.pageYOffset > offset) ? toTopButton.style.display = 'block' : toTopButton.style.display = 'none'; + }); + + toTopButton.addEventListener('click', function(event) { + event.preventDefault(); + window.scroll({ top: 0, left: 0, behavior: 'smooth' }); + }); +}); diff --git a/storage/js/crear-plan-nextcloud-ti.js b/storage/js/crear-plan-nextcloud-ti.js new file mode 100644 index 0000000..e69de29 diff --git a/storage/js/current-year.js b/storage/js/current-year.js new file mode 100644 index 0000000..e69de29 diff --git a/storage/js/custom-login-logo.js b/storage/js/custom-login-logo.js new file mode 100644 index 0000000..e69de29 diff --git a/storage/js/get-nextcloud-storage-via-reports-api.js b/storage/js/get-nextcloud-storage-via-reports-api.js new file mode 100644 index 0000000..e69de29 diff --git a/storage/js/nextcloud-banda-dynamic-pricing.js b/storage/js/nextcloud-banda-dynamic-pricing.js new file mode 100644 index 0000000..d934646 --- /dev/null +++ b/storage/js/nextcloud-banda-dynamic-pricing.js @@ -0,0 +1,1428 @@ +/** + * PMPro Banda Dynamic Pricing - JavaScript CON PRORRATEO VISUAL v2.8.0 + * + * Nombre del archivo: nextcloud-banda-dynamic-pricing.js + * + * RESPONSABILIDAD: Cálculos dinámicos de precio, prorrateo y actualización de UI + * CORREGIDO: Sistema completo de prorrateo para upgrades + * MEJORADO: Sanitización defensiva, control de doble init, bloqueo de downgrades + * + * @version 2.8.0 + */ + +/* global jQuery, window, console */ + +(function($) { + 'use strict'; + + // ==== + // CONFIGURACIÓN Y CONSTANTES + // ==== + + const BANDA_CONFIG = { + version: '2.8.0', + debug: false, + selectors: { + storageField: '#storage_space', + usersField: '#num_users', + frequencyField: '#payment_frequency', + priceDisplay: '#total_price_display', + priceLabel: '.pmpro_checkout-field-price-display label', + submitButtons: 'input[name="submit"], button[type="submit"], #pmpro_btn_submit' + }, + classes: { + proratedPrice: 'prorated-price', + proratedLabel: 'prorated-label', + proratedNotice: 'pmpro-prorated-notice', + downgradeWarning: 'pmpro-downgrade-warning', + messageNotice: 'pmpro-proration-message' + }, + debounceDelay: 100, + animationDuration: 50, + initTimeout: 1000 + }; + + // Variables globales + let pricingData = null; + let currentProrationData = null; + let debounceTimer = null; + let isCalculating = false; + let isInitialized = false; + let originalTextsCache = {}; + let initialUserValues = {}; + + // ==== + // SISTEMA DE LOGGING + // ==== + + function log(level, message, data = null) { + if (!BANDA_CONFIG.debug && level === 'debug') return; + + const logMessage = `[PMPro Banda ${BANDA_CONFIG.version}] ${message}`; + + if (level === 'error') { + console.error(logMessage, data || ''); + } else if (level === 'warn') { + console.warn(logMessage, data || ''); + } else if (level === 'info') { + console.info(logMessage, data || ''); + } else { + console.log(logMessage, data || ''); + } + } + + // ==== + // INICIALIZACIÓN Y VALIDACIÓN + // ==== + + function waitForPricingData(callback, timeoutMs = BANDA_CONFIG.initTimeout) { + if (typeof window.nextcloud_banda_pricing !== 'undefined') { + return callback(window.nextcloud_banda_pricing); + } + + let waited = 0; + const interval = setInterval(function() { + waited += 100; + if (typeof window.nextcloud_banda_pricing !== 'undefined') { + clearInterval(interval); + callback(window.nextcloud_banda_pricing); + } else if (waited >= timeoutMs) { + clearInterval(interval); + callback(null); + } + }, 100); + } + + function initializePricingSystem() { + if (isInitialized) { + log('debug', 'System already initialized, skipping'); + return true; + } + + log('info', 'Initializing pricing system...'); + + // Esperar a que los datos estén disponibles + waitForPricingData(function(config) { + if (!config) { + log('error', 'Pricing data not available'); + return false; + } + + pricingData = config; + BANDA_CONFIG.debug = pricingData.debug || false; + log('debug', 'Pricing data loaded', { + level_id: pricingData.level_id, + base_price: pricingData.base_price, + has_active_membership: pricingData.hasActiveMembership, + current_subscription_data: pricingData.current_subscription_data + }); + + if (!pricingData.level_id || parseInt(pricingData.level_id, 10) !== 2) { + log('debug', 'Not on Banda level, skipping initialization', { level_id: pricingData.level_id }); + return false; + } + + waitForRequiredElements(function(elementsFound) { + if (!elementsFound) { + log('warn', 'Required elements not found after timeout'); + return; + } + + initializeFieldValues(); + storeInitialUserValues(); + bindEvents(); + setInitialValues(); + + isInitialized = true; + + log('info', 'PMPro Banda Dynamic Pricing initialized successfully', { + version: BANDA_CONFIG.version, + debug: BANDA_CONFIG.debug, + hasActiveMembership: pricingData.hasActiveMembership, + currentSubscriptionData: !!pricingData.current_subscription_data + }); + + // Disparar evento de inicialización completada + $(document).trigger('nextcloud_banda_initialized'); + }); + }); + + return true; + } + + function waitForRequiredElements(callback, timeoutMs = 3000) { + const requiredElements = [ + BANDA_CONFIG.selectors.storageField, + BANDA_CONFIG.selectors.usersField, + BANDA_CONFIG.selectors.frequencyField, + BANDA_CONFIG.selectors.priceDisplay + ]; + + let waited = 0; + const interval = setInterval(function() { + const foundElements = requiredElements.filter(selector => $(selector).length > 0); + + if (foundElements.length >= 3) { + clearInterval(interval); + callback(true); + } else { + waited += 100; + if (waited >= timeoutMs) { + clearInterval(interval); + log('warn', 'Timeout waiting for elements', { + found: foundElements.length, + required: requiredElements.length + }); + callback(false); + } + } + }, 100); + } + + // ==== + // INICIALIZACIÓN DE CAMPOS + // ==== + + function initializeFieldValues() { + let defaultStorage = '1tb'; + let defaultUsers = 2; + let defaultFrequency = 'monthly'; + + if (pricingData.hasActiveMembership && pricingData.has_previous_config && pricingData.current_subscription_data) { + defaultStorage = pricingData.current_subscription_data.storage_space || '1tb'; + defaultUsers = pricingData.current_subscription_data.num_users || 2; + defaultFrequency = pricingData.current_subscription_data.payment_frequency || 'monthly'; + + log('debug', 'Using previous config values for active membership', { + storage: defaultStorage, + users: defaultUsers, + frequency: defaultFrequency + }); + } else { + log('debug', 'Using default values (no active membership or no previous config)', { + storage: defaultStorage, + users: defaultUsers, + frequency: defaultFrequency, + hasActiveMembership: pricingData.hasActiveMembership, + hasPreviousConfig: pricingData.has_previous_config + }); + } + + const $storageField = $(BANDA_CONFIG.selectors.storageField); + const $usersField = $(BANDA_CONFIG.selectors.usersField); + const $frequencyField = $(BANDA_CONFIG.selectors.frequencyField); + + if ($storageField.length && (!$storageField.val() || $storageField.val() === '')) { + $storageField.val(defaultStorage); + } + if ($usersField.length && (!$usersField.val() || $usersField.val() === '')) { + $usersField.val(defaultUsers); + } + if ($frequencyField.length && (!$frequencyField.val() || $frequencyField.val() === '')) { + $frequencyField.val(defaultFrequency); + } + + log('debug', 'Field values initialized', { + storage: $storageField.val(), + users: $usersField.val(), + frequency: $frequencyField.val() + }); + } + + function storeInitialUserValues() { + initialUserValues = { + storage: $(BANDA_CONFIG.selectors.storageField).val() || '1tb', + users: parseInt($(BANDA_CONFIG.selectors.usersField).val(), 10) || 2, + frequency: $(BANDA_CONFIG.selectors.frequencyField).val() || 'monthly', + hasPreviousConfig: !!(pricingData && pricingData.has_previous_config && pricingData.hasActiveMembership), + hasActiveMembership: !!(pricingData && pricingData.hasActiveMembership), + subscriptionData: pricingData.current_subscription_data || null + }; + + log('debug', 'Initial user values stored', initialUserValues); + } + + // ==== + // CÁLCULOS DE PRECIO + // ==== + + function calculatePrice(storageSpace, numUsers, paymentFrequency) { + if (!pricingData) { + log('error', 'Pricing data not available for calculation'); + return pricingData?.base_price || 70.0; + } + + try { + const sanitizedStorage = String(storageSpace || '1tb').toLowerCase(); + const sanitizedUsers = parseInt(numUsers, 10) || 2; + const sanitizedFrequency = String(paymentFrequency || 'monthly').toLowerCase(); + + if (!sanitizedStorage || !sanitizedUsers || !sanitizedFrequency) { + log('warn', 'Invalid parameters for price calculation', { + storageSpace: sanitizedStorage, + numUsers: sanitizedUsers, + paymentFrequency: sanitizedFrequency + }); + return pricingData.base_price; + } + + const storageTb = parseInt( + sanitizedStorage.replace('tb', '').replace('gb', ''), + 10 + ) || 1; + const users = sanitizedUsers; + + const additionalTb = Math.max(0, storageTb - pricingData.base_storage_included); + const storagePrice = pricingData.base_price + (pricingData.price_per_tb * additionalTb); + + const additionalUsers = Math.max(0, users - pricingData.base_users_included); + const userPrice = pricingData.price_per_user * additionalUsers; + + const combinedPrice = storagePrice + userPrice; + + const frequencyMultiplier = pricingData.frequency_multipliers[sanitizedFrequency]; + if (typeof frequencyMultiplier === 'undefined') { + log('warn', 'Unknown frequency, using 1.0 multiplier', { frequency: sanitizedFrequency }); + } + const multiplier = frequencyMultiplier || 1.0; + const totalPrice = Math.ceil(combinedPrice * multiplier); + + log('debug', 'Price calculated', { + storageSpace: sanitizedStorage, + storageTb, + additionalTb, + numUsers: users, + additionalUsers, + paymentFrequency: sanitizedFrequency, + storagePrice, + userPrice, + combinedPrice, + frequencyMultiplier: multiplier, + totalPrice + }); + + return totalPrice; + + } catch (error) { + log('error', 'Error calculating price', error); + return pricingData.base_price; + } + } + + // ==== + // SISTEMA DE PRORRATEO + // ==== + + function isUpgrade(newStorage, newUsers, newFrequency) { + if (!pricingData.current_subscription_data) { + log('debug', 'No current subscription data for upgrade check'); + return false; + } + + const current = pricingData.current_subscription_data; + + log('debug', 'Upgrade check input data', { + current: current, + new: { storage: newStorage, users: newUsers, frequency: newFrequency } + }); + + // Parsear storage correctamente + const parseStorageValue = (value) => { + if (typeof value !== 'string') return 1; + const sanitized = String(value).toLowerCase(); + const match = sanitized.match(/^(\d+(?:\.\d+)?)\s*(tb|gb)$/i); + if (match) { + const num = parseFloat(match[1]); + const unit = match[2].toLowerCase(); + return unit === 'gb' ? num / 1024 : num; + } + return parseFloat(value) || 1; + }; + + const currentStorageValue = parseStorageValue(current.storage_space || '1tb'); + const newStorageValue = parseStorageValue(newStorage); + + log('debug', 'Storage values parsed', { + currentStorageValue: currentStorageValue, + newStorageValue: newStorageValue + }); + + const currentUsers = parseInt(current.num_users || 2, 10); + const newUsersParsed = parseInt(newUsers, 10); + + log('debug', 'Users values parsed', { + currentUsers: currentUsers, + newUsersParsed: newUsersParsed + }); + + const frequencyOrder = { + 'monthly': 1, + 'semiannual': 2, + 'annual': 3, + 'biennial': 4, + 'triennial': 5, + 'quadrennial': 6, + 'quinquennial': 7 + }; + + const currentFreqOrder = frequencyOrder[current.payment_frequency] || 1; + const newFreqOrder = frequencyOrder[newFrequency] || 1; + + log('debug', 'Frequency orders', { + current: current.payment_frequency, + currentOrder: currentFreqOrder, + new: newFrequency, + newOrder: newFreqOrder + }); + + const storageUpgrade = newStorageValue > currentStorageValue; + const usersUpgrade = newUsersParsed > currentUsers; + const frequencyUpgrade = newFreqOrder > currentFreqOrder; + + const isUpgradeResult = storageUpgrade || usersUpgrade || frequencyUpgrade; + + log('debug', 'Upgrade analysis result', { + storageUpgrade: storageUpgrade, + usersUpgrade: usersUpgrade, + frequencyUpgrade: frequencyUpgrade, + isUpgrade: isUpgradeResult, + currentStorage: currentStorageValue, + newStorage: newStorageValue, + currentUser: currentUsers, + newUser: newUsersParsed + }); + + return isUpgradeResult; + } + + // Función auxiliar para sanitizar datos de prorrateo - CORREGIDA + function sanitizeProrationData(data, newTotalPrice) { + const safeNum = (n, fallback = 0) => { + const v = Number(n); + return Number.isFinite(v) ? v : fallback; + }; + + const safeInt = (n, fallback = 0) => { + const v = parseInt(n, 10); + return Number.isFinite(v) ? v : fallback; + }; + + if (!data || typeof data !== 'object') { + return { + raw: null, + isUpgrade: false, + shouldDisplay: false, + message: 'Dados de prorrateo indisponíveis.', + priceDiff: 0, + amountDueNow: safeNum(newTotalPrice, 0), + creditAmount: 0, + newTotalPrice: safeNum(newTotalPrice, 0), + currentAmount: 0, + currentProratedAmount: 0, + newProratedAmount: 0, + fractionRemaining: 0, + daysRemaining: 0, + totalDays: 1, + nextPaymentDate: '', + currentFrequency: 'monthly', + newFrequency: 'monthly', + currentCycleLabel: '', + newCycleLabel: '' + }; + } + + const amountDueNow = safeNum( + data.amount_due_now ?? + data.amountDueNow ?? + data.prorated_amount ?? + data.proratedAmount ?? + NaN, + NaN + ); + + const creditAmount = safeNum( + data.credit_amount ?? + data.creditAmount ?? + data.current_prorated_amount ?? + data.currentProratedAmount ?? + 0 + ); + + const sanitized = { + raw: data, + isUpgrade: Boolean( + data.is_upgrade ?? + data.isUpgrade ?? + (safeNum(data.price_diff ?? data.priceDiff, 0) > 0) ?? + (Number.isFinite(amountDueNow) && amountDueNow > 0) + ), + shouldDisplay: Boolean(data.success ?? data.shouldDisplay ?? false), + message: String(data.message ?? data.Message ?? ''), + priceDiff: safeNum(data.price_diff ?? data.priceDiff ?? 0), + amountDueNow: Number.isFinite(amountDueNow) ? amountDueNow : safeNum(newTotalPrice, 0), + creditAmount: creditAmount, + newTotalPrice: safeNum(data.new_total_price ?? data.newTotalPrice ?? newTotalPrice, 0), + currentAmount: safeNum(data.current_price ?? data.currentAmount ?? 0), + currentProratedAmount: safeNum(data.current_prorated_amount ?? data.currentProratedAmount ?? creditAmount), + newProratedAmount: safeNum(data.new_prorated_amount ?? data.newProratedAmount ?? data.prorated_new_amount ?? 0), + fractionRemaining: safeNum(data.fraction_remaining ?? data.fractionRemaining ?? 0), + daysRemaining: safeInt(data.days_remaining ?? data.daysRemaining ?? 0), + totalDays: Math.max(1, safeInt(data.total_days ?? data.totalDays ?? 1)), + nextPaymentDate: String(data.next_payment_date ?? data.nextPaymentDate ?? ''), + currentFrequency: String(data.current_frequency ?? data.currentFrequency ?? 'monthly'), + newFrequency: String(data.new_frequency ?? data.newFrequency ?? 'monthly'), + currentCycleLabel: String(data.current_cycle_label ?? data.currentCycleLabel ?? ''), + newCycleLabel: String(data.new_cycle_label ?? data.newCycleLabel ?? '') + }; + + sanitized.shouldDisplay = + sanitized.shouldDisplay || + sanitized.amountDueNow > 0 || + (sanitized.message && sanitized.message !== 'Success'); + + return sanitized; + } + + // Modificar calculateProration para manejo de errores del spinner + function calculateProration(newTotalPrice, callback) { + if (!pricingData.hasActiveMembership || !pricingData.current_subscription_data) { + log('debug', 'No active membership for proration', { + hasActiveMembership: pricingData?.hasActiveMembership, + hasSubscriptionData: !!pricingData?.current_subscription_data + }); + hideSpinner(); // Asegurar que el spinner se oculte + callback(null); + return; + } + + const storageSpace = $(BANDA_CONFIG.selectors.storageField).val(); + const numUsers = $(BANDA_CONFIG.selectors.usersField).val(); + const paymentFrequency = $(BANDA_CONFIG.selectors.frequencyField).val(); + + // Verificar que los valores sean válidos + if (!storageSpace || !numUsers || !paymentFrequency) { + log('warn', 'Missing field values for proration calculation', { + storageSpace, numUsers, paymentFrequency + }); + hideSpinner(); // Asegurar que el spinner se oculte + callback(null); + return; + } + + log('debug', 'Starting AJAX proration calculation', { + action: 'nextcloud_banda_calculate_proration', + level_id: pricingData.level_id, + storage: storageSpace, + users: numUsers, + frequency: paymentFrequency + }); + + $.ajax({ + url: pricingData.ajax_url, + type: 'POST', + dataType: 'json', + timeout: 10000, + data: { + action: 'nextcloud_banda_calculate_proration', + security: pricingData.nonce, + level_id: pricingData.level_id, + storage: storageSpace, + users: numUsers, + frequency: paymentFrequency + }, + success: function(response) { + try { + log('debug', 'Proration AJAX response received', { + success: response?.success, + hasData: !!response?.data, + rawData: response + }); + + if (!response) { + log('warn', 'Empty AJAX response'); + hideSpinner(); // Ocultar spinner en caso de error + callback(null); + return; + } + + // Manejar respuesta de éxito o error + if (response.success) { + const data = response.data || response; + + if (!data || typeof data !== 'object') { + log('warn', 'Invalid data structure in response', { response }); + hideSpinner(); // Ocultar spinner en caso de error + callback(null); + return; + } + + // Sanitizar y procesar datos + const prorationResult = sanitizeProrationData(data, newTotalPrice); + log('debug', 'Processed proration result', prorationResult); + callback(prorationResult); + + } else { + // Manejar errores del servidor + const errorMessage = response.data?.message || 'Erro no cálculo de prorrateo'; + log('warn', 'Server error in proration calculation', { + message: errorMessage, + data: response.data + }); + + callback({ + isUpgrade: false, + shouldDisplay: true, + message: errorMessage, + priceDiff: 0, + proratedAmount: 0, + newTotalPrice: newTotalPrice, + currentAmount: 0, + currentProratedAmount: 0, + newProratedAmount: 0, + fractionRemaining: 0, + daysRemaining: 0, + totalDays: 1, + nextPaymentDate: '', + currentFrequency: 'monthly', + newFrequency: 'monthly', + raw: response.data + }); + } + } catch (error) { + log('error', 'Error processing proration response', { + error: error.message, + stack: error.stack + }); + hideSpinner(); // Ocultar spinner en caso de error + callback(null); + } + }, + error: function(xhr, status, error) { + log('error', 'AJAX error calculating proration', { + status: status, + error: error, + responseText: xhr.responseText, + statusCode: xhr.status + }); + + // Manejar error de red o timeout + let errorMessage = 'Não foi possível calcular o prorrateo. '; + if (status === 'timeout') { + errorMessage += 'Tempo limite excedido.'; + } else if (xhr.status === 401) { + errorMessage += 'Usuário não autenticado.'; + } else if (xhr.status === 403) { + errorMessage += 'Acesso negado.'; + } else { + errorMessage += 'Tente novamente.'; + } + + callback({ + isUpgrade: false, + shouldDisplay: true, + message: errorMessage, + priceDiff: 0, + proratedAmount: 0, + newTotalPrice: newTotalPrice, + currentAmount: 0, + currentProratedAmount: 0, + newProratedAmount: 0, + fractionRemaining: 0, + daysRemaining: 0, + totalDays: 1, + nextPaymentDate: '', + currentFrequency: 'monthly', + newFrequency: 'monthly', + raw: null + }); + }, + complete: function() { + // Asegurar que el spinner se oculte siempre al completar la llamada AJAX + hideSpinner(); + } + }); + } + + // ==== + // SPINNER FUNCTIONS + // ==== + + function showSpinner() { + // Crear spinner si no existe + let $spinner = $('#banda-pricing-spinner'); + if ($spinner.length === 0) { + const spinnerHtml = ` + + + `; + const $priceField = $(BANDA_CONFIG.selectors.priceDisplay).closest('.pmpro_checkout-field-price-display'); + if ($priceField.length > 0) { + $priceField.after(spinnerHtml); + } else { + $('.pmpro_form').prepend(spinnerHtml); + } + $spinner = $('#banda-pricing-spinner'); + } + + $spinner.fadeIn(200); + log('debug', 'Spinner shown'); + } + + function hideSpinner() { + const $spinner = $('#banda-pricing-spinner'); + if ($spinner.length > 0) { + $spinner.fadeOut(200); + log('debug', 'Spinner hidden'); + } + } + + // ==== + // ACTUALIZACIÓN DE UI (VERSIÓN SEGURA) - CORREGIDA + // ==== + + function updatePriceDisplaySafe(price, prorationData = null) { + const $priceField = $(BANDA_CONFIG.selectors.priceDisplay); + const $priceLabel = $(BANDA_CONFIG.selectors.priceLabel); + + if (!$priceField.length) { + log('warn', 'Price display field not found'); + return; + } + + const toFloat = (value, fallback = null) => { + if (value === null || typeof value === 'undefined') { + return fallback; + } + + let normalized = value; + + if (typeof normalized === 'string') { + normalized = normalized.replace(/\s+/g, ''); + normalized = normalized.replace(/[^\d,.-]/g, ''); + + const commaCount = (normalized.match(/,/g) || []).length; + const dotCount = (normalized.match(/\./g) || []).length; + + if (commaCount > 0 && dotCount > 0) { + if (normalized.lastIndexOf(',') > normalized.lastIndexOf('.')) { + normalized = normalized.replace(/\./g, '').replace(',', '.'); + } else { + normalized = normalized.replace(/,/g, ''); + } + } else if (commaCount > 0 && dotCount === 0) { + normalized = normalized.replace(',', '.'); + } + } + + const parsed = Number(normalized); + return Number.isFinite(parsed) ? parsed : fallback; + }; + + const safeNum = (n, fallback = 0) => { + const num = toFloat(n, null); + if (num === null) { + return fallback; + } + const min = toFloat(fallback, 0); + return Math.max(min, num); + }; + + $priceField.removeClass(BANDA_CONFIG.classes.proratedPrice); + $priceLabel.removeClass(BANDA_CONFIG.classes.proratedLabel); + $('.' + BANDA_CONFIG.classes.proratedNotice).remove(); + $('.' + BANDA_CONFIG.classes.downgradeWarning).remove(); + $('.' + BANDA_CONFIG.classes.messageNotice).remove(); + + let displayPrice = safeNum(price); + let labelText = 'Preço total'; + let shouldShowProration = false; + + if (prorationData && prorationData.shouldDisplay) { + const amountDueNow = toFloat(prorationData.amountDueNow, null); + const creditAmount = safeNum( + prorationData.creditAmount ?? prorationData.currentProratedAmount ?? 0, + 0 + ); + + if (amountDueNow !== null && amountDueNow >= 0) { + displayPrice = amountDueNow; + labelText = 'Valor a pagar agora'; + shouldShowProration = true; + } else if (prorationData.isUpgrade) { + displayPrice = safeNum(prorationData.newTotalPrice, displayPrice); + labelText = 'Valor a pagar agora'; + shouldShowProration = true; + } else { + displayPrice = safeNum(prorationData.newTotalPrice, displayPrice); + labelText = 'Preço total'; + shouldShowProration = + Boolean(prorationData.message && prorationData.message !== 'Success'); + } + + if (shouldShowProration) { + $priceField.addClass(BANDA_CONFIG.classes.proratedPrice); + $priceLabel.addClass(BANDA_CONFIG.classes.proratedLabel); + } + + const newTotalPrice = safeNum(prorationData.newTotalPrice, displayPrice); + const daysRemaining = Math.max(0, parseInt(prorationData.daysRemaining, 10) || 0); + const totalDays = Math.max(1, parseInt(prorationData.totalDays, 10) || 1); + + const fraction = Number.isFinite(prorationData.fractionRemaining) + ? prorationData.fractionRemaining + : (totalDays > 0 ? daysRemaining / totalDays : 0); + const fractionPercent = (fraction * 100).toFixed(2); + + const currentFrequencyLabel = + prorationData.currentCycleLabel || + getFrequencyLabel(prorationData.currentFrequency || 'monthly'); + const newFrequencyLabel = + prorationData.newCycleLabel || + getFrequencyLabel(prorationData.newFrequency || 'monthly'); + + let noticeContent = ''; + + if (prorationData.isUpgrade && amountDueNow !== null && amountDueNow >= 0) { + noticeContent = ` +

🚀 Upgrade da configuração

+
+ Preço da nova configuração: R$ ${formatPrice(newTotalPrice)} +
+
+ Ciclo de pagamento atual: ${currentFrequencyLabel} +
+
+ Novo ciclo de pagamento: ${newFrequencyLabel} +
+
+ Período restante: ${daysRemaining} dias (${fractionPercent}% do ciclo) +
+
+
+ Crédito por dias não utilizados: -R$ ${formatPrice(creditAmount)} +
+
+ Valor a pagar agora: R$ ${formatPrice(displayPrice)} +
+
+
+ 💡 Você paga apenas a diferença prorratada agora.
➡️ O valor integral da nova configuração só será cobrado no próximo ciclo. +
+ `; + } else if (prorationData.isUpgrade) { + noticeContent = ` +

🚀 Upgrade da configuração

+
+ Preço da nova configuração: R$ ${formatPrice(newTotalPrice)} +
+
+ Ciclo de pagamento atual: ${currentFrequencyLabel} +
+
+ Novo ciclo de pagamento: ${newFrequencyLabel} +
+ ${ + daysRemaining > 0 + ? `
+ Período restante: ${daysRemaining} dias +
` + : '' + } +
+ Valor a pagar agora: R$ ${formatPrice(displayPrice)} +
+
+ 🔄 Esta é uma atualização da sua configuração. +
+ `; + } else { + noticeContent = ` +

📋 Detalhes da sua configuração

+
+ Preço total da configuração: R$ ${formatPrice(newTotalPrice)} +
+
+ Ciclo de pagamento: ${newFrequencyLabel} +
+ ${ + daysRemaining > 0 + ? `
+ Período restante do ciclo atual: ${daysRemaining} dias +
` + : '' + } +
+ ℹ️ Esta é a configuração que você selecionou. +
+ `; + } + + const noticeHtml = ` +
+ ${noticeContent} +
+ `; + + const $targetElement = $priceField.closest('.pmpro_checkout-field-price-display'); + if ($targetElement.length > 0) { + $targetElement.after(noticeHtml); + } else { + $priceField.after(noticeHtml); + } + + if (prorationData.message && prorationData.message !== 'Success') { + const messageHtml = ` +
+ ℹ️ ${prorationData.message} +
+ `; + $priceField.closest('.pmpro_checkout-field-price-display').after(messageHtml); + } + } else if (prorationData && prorationData.message && prorationData.message !== 'Success') { + const messageHtml = ` +
+ ℹ️ ${prorationData.message} +
+ `; + $priceField.closest('.pmpro_checkout-field-price-display').after(messageHtml); + } + + $priceField.fadeOut(BANDA_CONFIG.animationDuration / 2, function () { + $(this) + .val('R$ ' + formatPrice(displayPrice)) + .fadeIn(BANDA_CONFIG.animationDuration / 2); + }); + + if ($priceLabel.length) { + $priceLabel.text(labelText); + } + + log('debug', 'Price display updated completely', { + finalPrice: displayPrice, + labelText: labelText, + }); + } + + /** + * Convierte frequency key a etiqueta legible en portugués + */ + function getFrequencyLabel(frequencyKey) { + const labels = { + 'monthly': 'Mensal', + 'semiannual': 'Semestral', + 'annual': 'Anual', + 'biennial': 'Bienal', + 'triennial': 'Trienal', + 'quadrennial': 'Quadrienal', + 'quinquennial': 'Quinquenal', + 'weekly': 'Semanal', + 'biweekly': 'Quinzenal', + 'daily': 'Diário' + }; + + return labels[frequencyKey] || frequencyKey; + } + + /** + * Alias para compatibilidad con scripts existentes. + * Mantiene la firma original y dispara eventos públicos. + */ + function updatePriceDisplay(price, prorationData = null, options = {}) { + if (!options || typeof options !== 'object') { + options = {}; + } + + updatePriceDisplaySafe(price, prorationData); + + log('debug', 'updatePriceDisplay alias invoked', { + price, + hasProration: !!(prorationData && (prorationData.isUpgrade || prorationData.shouldDisplay)), + options + }); + + $(document).trigger('nextcloud_banda_price_updated', [price, prorationData, options]); + } + + if (typeof window !== 'undefined') { + window.updatePriceDisplaySafe = updatePriceDisplaySafe; + window.updatePriceDisplay = updatePriceDisplay; + } + + function formatPrice(price) { + const n = Number(price); + const safe = Number.isFinite(n) ? n : 0; + return safe.toLocaleString('pt-BR', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + } + + // ==== + // BLOQUEO DE DOWNGRADES + // ==== + + function updateFieldOptions() { + if (!initialUserValues.hasPreviousConfig) return; + + updateStorageOptions(); + updateUserOptions(); + updateFrequencyOptions(); + } + + function updateStorageOptions() { + const $storageSelect = $(BANDA_CONFIG.selectors.storageField); + if (!$storageSelect.length) return; + + const currentStorageValue = parseStorageValue(initialUserValues.storage); + + $storageSelect.find('option').each(function() { + const $option = $(this); + const optionValue = parseStorageValue($option.val()); + const key = 'storage_space_' + $option.val(); + + if (!originalTextsCache[key]) { + originalTextsCache[key] = $option.text(); + } + + if (optionValue < currentStorageValue) { + $option.prop('disabled', true); + $option.text(originalTextsCache[key] + ' (Não elegível)'); + } else { + $option.prop('disabled', false); + $option.text(originalTextsCache[key]); + } + }); + } + + function updateUserOptions() { + const $userSelect = $(BANDA_CONFIG.selectors.usersField); + if (!$userSelect.length) return; + + const currentUsers = initialUserValues.users; + + $userSelect.find('option').each(function() { + const $option = $(this); + const optionValue = parseInt($option.val(), 10); + const key = 'num_users_' + $option.val(); + + if (!originalTextsCache[key]) { + originalTextsCache[key] = $option.text(); + } + + if (optionValue < currentUsers) { + $option.prop('disabled', true); + $option.text(originalTextsCache[key] + ' (Não elegível)'); + } else { + $option.prop('disabled', false); + $option.text(originalTextsCache[key]); + } + }); + } + + function updateFrequencyOptions() { + const $frequencySelect = $(BANDA_CONFIG.selectors.frequencyField); + if (!$frequencySelect.length) return; + + const frequencyOrder = { + monthly: 1, + semiannual: 2, + annual: 3, + biennial: 4, + triennial: 5, + quadrennial: 6, + quinquennial: 7 + }; + + const currentFrequency = initialUserValues.frequency || 'monthly'; + const currentFreqOrder = frequencyOrder[currentFrequency] || 1; + + $frequencySelect.find('option').each(function() { + const $option = $(this); + const optionValue = $option.val(); + const optionOrder = frequencyOrder[optionValue] || 0; + const key = 'payment_frequency_' + optionValue; + + if (!originalTextsCache[key]) { + originalTextsCache[key] = $option.text(); + } + + if (optionOrder < currentFreqOrder) { + $option.prop('disabled', true); + $option.text(originalTextsCache[key] + ' (Não elegível)'); + } else { + $option.prop('disabled', false); + $option.text(originalTextsCache[key]); + } + }); + + log('debug', 'Frequency options updated', { + currentFrequency, + currentFreqOrder + }); + } + + function parseStorageValue(value) { + if (typeof value !== 'string') return 1; + const sanitized = String(value).toLowerCase(); + const match = sanitized.match(/^(\d+(?:\.\d+)?)\s*(tb|gb)$/i); + if (match) { + const num = parseFloat(match[1]); + const unit = match[2].toLowerCase(); + return unit === 'gb' ? num / 1024 : num; + } + return parseFloat(value) || 1; + } + + function showDowngradeWarning(reason) { + $('.' + BANDA_CONFIG.classes.downgradeWarning).remove(); + + const $warning = $(` +
+ ⚠️ Ação bloqueada: ${reason} +
+ `); + + const $priceField = $(BANDA_CONFIG.selectors.priceDisplay).closest('.pmpro_checkout-field-price-display'); + if ($priceField.length) { + $warning.insertBefore($priceField); + } else { + $('.pmpro_form').prepend($warning); + } + + $(BANDA_CONFIG.selectors.submitButtons).prop('disabled', true).css('opacity', '0.6'); + + log('debug', 'Downgrade warning applied', { reason }); + } + + function clearDowngradeWarning() { + $('.' + BANDA_CONFIG.classes.downgradeWarning).remove(); + $(BANDA_CONFIG.selectors.submitButtons).prop('disabled', false).css('opacity', '1'); + } + + // ==== + // MANEJO DE EVENTOS + // ==== + + // También actualizar la función handleFieldChange para mostrar el spinner inmediatamente + function handleFieldChange() { + if (isCalculating) { + log('debug', 'Calculation already in progress, skipping'); + return; + } + + if (initialUserValues.hasPreviousConfig && hasUserMadeChanges()) { + if (isDowngradeAttempt()) { + showDowngradeWarning('Downgrades não são permitidos. Entre em contato com o suporte para alterações.'); + return; + } else { + clearDowngradeWarning(); + updateFieldOptions(); + } + } + + clearTimeout(debounceTimer); + debounceTimer = setTimeout(function() { + showSpinner(); // Mostrar spinner inmediatamente al iniciar el debounce + performPriceCalculation(); + }, BANDA_CONFIG.debounceDelay); + } + + // Modificar performPriceCalculation para incluir spinner + function performPriceCalculation() { + isCalculating = true; + showSpinner(); // Mostrar spinner al inicio del cálculo + + const storageSpace = $(BANDA_CONFIG.selectors.storageField).val(); + const numUsers = $(BANDA_CONFIG.selectors.usersField).val(); + const paymentFrequency = $(BANDA_CONFIG.selectors.frequencyField).val(); + + log('debug', 'Performing price calculation', { + storageSpace, + numUsers, + paymentFrequency, + hasActiveMembership: pricingData?.hasActiveMembership, + hasCurrentSubscriptionData: !!pricingData?.current_subscription_data, + pricingDataAvailable: !!pricingData + }); + + const newPrice = calculatePrice(storageSpace, numUsers, paymentFrequency); + + // Debugging adicional antes del prorrateo + if (pricingData) { + const shouldCalculateProration = pricingData.hasActiveMembership && pricingData.current_subscription_data; + const isUpgradeCheck = isUpgrade(storageSpace, numUsers, paymentFrequency); + + log('debug', 'Proration eligibility check', { + shouldCalculate: shouldCalculateProration, + isUpgrade: isUpgradeCheck, + hasActiveMembership: pricingData.hasActiveMembership, + hasSubscriptionData: !!pricingData.current_subscription_data + }); + + if (shouldCalculateProration && isUpgradeCheck) { + calculateProration(newPrice, function(prorationData) { + log('debug', 'Proration callback received', { + hasData: !!prorationData, + isUpgrade: prorationData?.isUpgrade, + shouldDisplay: prorationData?.shouldDisplay, + proratedAmount: prorationData?.proratedAmount + }); + + currentProrationData = prorationData; + updatePriceDisplay(newPrice, prorationData); + isCalculating = false; + hideSpinner(); // Ocultar spinner al finalizar + }); + } else { + log('debug', 'Skipping proration calculation - conditions not met'); + // Solo mostrar información básica si es upgrade pero no se puede calcular prorrateo + if (isUpgradeCheck) { + const basicProrationData = { + isUpgrade: true, + shouldDisplay: true, + message: 'Atualização de configuração detectada', + priceDiff: 0, + proratedAmount: newPrice, + newTotalPrice: newPrice, + currentAmount: 0, + currentProratedAmount: 0, + newProratedAmount: 0, + fractionRemaining: 0, + daysRemaining: 0, + totalDays: 1, + nextPaymentDate: '', + currentFrequency: paymentFrequency, + newFrequency: paymentFrequency, + raw: null + }; + updatePriceDisplay(newPrice, basicProrationData); + } else { + updatePriceDisplay(newPrice, null); + } + isCalculating = false; + hideSpinner(); // Ocultar spinner al finalizar + } + } else { + log('debug', 'No pricing data available, updating display without proration'); + updatePriceDisplay(newPrice, null); + isCalculating = false; + hideSpinner(); // Ocultar spinner al finalizar + } + } + + function hasUserMadeChanges() { + if (!initialUserValues.hasPreviousConfig) return false; + + const currentStorage = $(BANDA_CONFIG.selectors.storageField).val(); + const currentUsers = $(BANDA_CONFIG.selectors.usersField).val(); + const currentFrequency = $(BANDA_CONFIG.selectors.frequencyField).val(); + + return ( + currentStorage !== initialUserValues.storage || + currentUsers !== initialUserValues.users.toString() || + currentFrequency !== initialUserValues.frequency + ); + } + + function isDowngradeAttempt() { + if (!initialUserValues.hasPreviousConfig) return false; + + const currentStorage = parseStorageValue(initialUserValues.storage); + const selectedStorage = parseStorageValue($(BANDA_CONFIG.selectors.storageField).val()); + const currentUsers = initialUserValues.users; + const selectedUsers = parseInt($(BANDA_CONFIG.selectors.usersField).val(), 10); + + const frequencyOrder = { + monthly: 1, + semiannual: 2, + annual: 3, + biennial: 4, + triennial: 5, + quadrennial: 6, + quinquennial: 7 + }; + const currentFreqOrder = frequencyOrder[initialUserValues.frequency] || 1; + const selectedFreqOrder = frequencyOrder[$(BANDA_CONFIG.selectors.frequencyField).val()] || 1; + + return ( + selectedStorage < currentStorage || + selectedUsers < currentUsers || + selectedFreqOrder < currentFreqOrder + ); + } + + // ==== + // CONFIGURACIÓN INICIAL DE CAMPOS + // ==== + + function setInitialValues() { + if (!pricingData) return; + + log('debug', 'Setting initial values', { + current_storage: pricingData.current_storage, + current_users: pricingData.current_users, + current_frequency: pricingData.current_frequency, + has_previous_config: pricingData.has_previous_config + }); + + performPriceCalculation(); + + if (initialUserValues.hasPreviousConfig) { + updateFieldOptions(); + } + } + + function bindEvents() { + log('debug', 'Binding events to form fields'); + + $(document) + .off('.pmproband') + .on('change.pmproband', BANDA_CONFIG.selectors.storageField, handleFieldChange) + .on('change.pmproband', BANDA_CONFIG.selectors.usersField, handleFieldChange) + .on('change.pmproband', BANDA_CONFIG.selectors.frequencyField, handleFieldChange); + + $(document).on('submit.pmproband', 'form#pmpro_form', function() { + if (currentProrationData && (currentProrationData.isUpgrade || currentProrationData.shouldDisplay)) { + log('info', 'Form submitted with proration data', currentProrationData); + } + clearDowngradeWarning(); + }); + + log('debug', 'Events bound successfully'); + } + + // ==== + // SISTEMA DE TOOLTIPS (OPCIONAL) + // ==== + + function initializeTooltips() { + const tooltipData = [ + { + field: BANDA_CONFIG.selectors.storageField, + text: 'Selecione o espaço de armazenamento necessário para sua equipe. 1TB está incluído no plano base.' + }, + { + field: BANDA_CONFIG.selectors.usersField, + text: 'Número de usuários que terão acesso ao Nextcloud. 2 usuários estão incluídos no plano base.' + }, + { + field: BANDA_CONFIG.selectors.frequencyField, + text: 'Escolha a frequência de pagamento. Planos mais longos oferecem desconto progressivo.' + } + ]; + + tooltipData.forEach(function(tooltip) { + const $field = $(tooltip.field); + const $label = $field.closest('.pmpro_checkout-field-price-display').find('label'); + + if ($label.length && !$label.find('.pmpro-tooltip-trigger').length) { + $label.append(` `); + } + }); + } + + // ==== + // INICIALIZACIÓN PRINCIPAL + // ==== + + function main() { + log('info', 'PMPro Banda Dynamic Pricing starting...'); + + if (typeof $ === 'undefined') { + log('error', 'jQuery not available'); + return; + } + + if (!initializePricingSystem()) { + log('info', 'Pricing system not initialized (not applicable for current context)'); + return; + } + + initializeTooltips(); + } + + // ==== + // PUNTO DE ENTRADA + // ==== + + $(document).ready(function() { + if (isInitialized) return; + + waitForPricingData(function(config) { + if (!config) { + log('warn', 'Pricing data not found after wait; initialization skipped'); + return; + } + + setTimeout(main, 100); + }); + }); + + if (typeof window !== 'undefined') { + $(document).ready(function() { + if (pricingData?.debug || false) { + window.BandaPricingDebug = { + calculatePrice: calculatePrice, + calculateProration: calculateProration, + isUpgrade: isUpgrade, + currentProrationData: function() { return currentProrationData; }, + pricingData: function() { return pricingData; }, + initialUserValues: function() { return initialUserValues; }, + version: BANDA_CONFIG.version, + isInitialized: function() { return isInitialized; }, + updatePriceDisplaySafe, + updatePriceDisplay + }; + log('debug', 'Debug functions exposed to window.BandaPricingDebug'); + } + }); + } + + // Función de diagnóstico para verificar el estado del sistema + function diagnosePricingSystem() { + console.group('=== PMPro Banda Pricing System Diagnosis ==='); + + console.log('Window pricing data:', window.nextcloud_banda_pricing); + console.log('Has pricing data:', !!window.nextcloud_banda_pricing); + + if (window.nextcloud_banda_pricing) { + console.log('Has active membership:', window.nextcloud_banda_pricing.hasActiveMembership); + console.log('Has subscription data:', !!window.nextcloud_banda_pricing.current_subscription_data); + console.log('Current subscription data:', window.nextcloud_banda_pricing.current_subscription_data); + } + + console.log('Required DOM elements:'); + console.log('Storage field:', $(BANDA_CONFIG.selectors.storageField).length); + console.log('Users field:', $(BANDA_CONFIG.selectors.usersField).length); + console.log('Frequency field:', $(BANDA_CONFIG.selectors.frequencyField).length); + console.log('Price display:', $(BANDA_CONFIG.selectors.priceDisplay).length); + + console.log('Is initialized:', isInitialized); + console.log('Pricing data loaded:', !!pricingData); + + if (pricingData) { + console.log('Pricing data details:', { + hasActiveMembership: pricingData.hasActiveMembership, + hasPreviousConfig: pricingData.has_previous_config, + currentSubscriptionData: !!pricingData.current_subscription_data, + levelId: pricingData.level_id + }); + } + + console.groupEnd(); + } + + // Exponer para debugging + if (typeof window !== 'undefined' && (window.nextcloud_banda_pricing?.debug || false)) { + window.diagnoseBandaPricing = diagnosePricingSystem; + } + +})(jQuery); diff --git a/storage/js/pmpro-dynamic-pricing.js b/storage/js/pmpro-dynamic-pricing.js new file mode 100644 index 0000000..d0e800a --- /dev/null +++ b/storage/js/pmpro-dynamic-pricing.js @@ -0,0 +1,660 @@ +/** + * 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'); + } +}); diff --git a/storage/js/pmpro-hide-toolbar.js b/storage/js/pmpro-hide-toolbar.js new file mode 100644 index 0000000..e69de29 diff --git a/storage/js/pmpro-pricing-tables.js b/storage/js/pmpro-pricing-tables.js new file mode 100644 index 0000000..e69de29 diff --git a/storage/js/simple-accordion.js b/storage/js/simple-accordion.js new file mode 100644 index 0000000..8c91467 --- /dev/null +++ b/storage/js/simple-accordion.js @@ -0,0 +1,53 @@ +document.addEventListener('DOMContentLoaded', () => { + initSimpleAccordion(); + + function initSimpleAccordion() { + const accordionHeaders = document.querySelectorAll('.simple-accordion .accordion-header'); + + accordionHeaders.forEach(header => { + header.addEventListener('click', () => { + toggleAccordion(header); + }); + + // Accesibilidad: navegación por teclado + header.addEventListener('keydown', e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleAccordion(header); + } + }); + }); + } + + function toggleAccordion(header) { + const content = header.nextElementSibling; + const accordion = header.closest('.simple-accordion'); + const isActive = header.classList.contains('active'); + + // Cerrar otros acordeones si no es la versión de múltiples abiertos + if (!accordion.classList.contains('allow-multiple-open')) { + accordion.querySelectorAll('.accordion-header').forEach(h => { + if (h !== header && h.classList.contains('active')) { + h.classList.remove('active'); + h.setAttribute('aria-expanded', 'false'); + h.nextElementSibling.classList.remove('active'); + } + }); + } + + // Toggle del acordeón actual + header.classList.toggle('active'); + content.classList.toggle('active'); + header.setAttribute('aria-expanded', !isActive); + + // Scroll suave al acordeón abierto + if (!isActive) { + content.addEventListener('transitionend', () => { + header.scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + }, { once: true }); + } + } +}); diff --git a/storage/js/smtp-settings.js b/storage/js/smtp-settings.js new file mode 100644 index 0000000..e69de29 diff --git a/storage/js/theme-scripts.js b/storage/js/theme-scripts.js new file mode 100644 index 0000000..c9f5075 --- /dev/null +++ b/storage/js/theme-scripts.js @@ -0,0 +1 @@ +// Tu código JS aquí diff --git a/storage/snippets/auto-redirect-after-logout.json b/storage/snippets/auto-redirect-after-logout.json new file mode 100644 index 0000000..88f0585 --- /dev/null +++ b/storage/snippets/auto-redirect-after-logout.json @@ -0,0 +1,14 @@ +{ + "description": "Redirect to Homepage after logout", + "last_updated": "2025-08-15 03:24:01", + "active": true, + "hooks": { + "wp_logout": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + } + }, + "load_priority": 10 +} \ No newline at end of file diff --git a/storage/snippets/auto-redirect-after-logout.php b/storage/snippets/auto-redirect-after-logout.php new file mode 100644 index 0000000..b29b7d5 --- /dev/null +++ b/storage/snippets/auto-redirect-after-logout.php @@ -0,0 +1,12 @@ +'; diff --git a/storage/snippets/crear-plan-nextcloud-ti.json b/storage/snippets/crear-plan-nextcloud-ti.json new file mode 100644 index 0000000..750485e --- /dev/null +++ b/storage/snippets/crear-plan-nextcloud-ti.json @@ -0,0 +1,7 @@ +{ + "description": "Responder a solicitud de plan Nextcloud TI integrada con pricing din\u00e1mico", + "last_updated": "2025-08-25 00:50:51", + "active": true, + "hooks": [], + "load_priority": 10 +} \ No newline at end of file diff --git a/storage/snippets/crear-plan-nextcloud-ti.php b/storage/snippets/crear-plan-nextcloud-ti.php new file mode 100644 index 0000000..9522464 --- /dev/null +++ b/storage/snippets/crear-plan-nextcloud-ti.php @@ -0,0 +1,420 @@ + $user_id, + 'morder_exists' => !empty($morder) + ]); + return false; + } + + try { + // Generar password para la nueva cuenta Nextcloud + $password = wp_generate_password(12, false); + + // Obtener información del usuario con validaciones + $user = get_userdata($user_id); + if (!$user) { + nextcloud_log_error('User not found', ['user_id' => $user_id]); + return false; + } + + $email = $user->user_email; + $username = $user->user_login; + $displayname = $user->display_name ?: $username; + + // Obtener nivel de membresía actual + $level = pmpro_getMembershipLevelForUser($user_id); + if (!$level) { + nextcloud_log_error('No membership level found for user', ['user_id' => $user_id]); + return false; + } + + // Configurar timezone y fecha del pedido + $dt = new DateTime(); + $dt->setTimezone(new DateTimeZone('America/Boa_Vista')); + + // Usar timestamp del morder o timestamp actual + $order_timestamp = !empty($morder->timestamp) ? $morder->timestamp : current_time('timestamp'); + $dt->setTimestamp($order_timestamp); + $fecha_pedido = $dt->format('d/m/Y H:i:s'); + + // Obtener configuración dinámica del usuario (del sistema de pricing dinámico) + $config_data = get_nextcloud_user_config($user_id); + + // Obtener fecha del próximo pago usando PMPro + $fecha_pago_proximo = get_pmpro_next_payment_date($user_id, $level); + + // Preparar datos del email + $email_data = prepare_nextcloud_email_data($user, $level, $morder, $config_data, [ + 'password' => $password, + 'fecha_pedido' => $fecha_pedido, + 'fecha_pago_proximo' => $fecha_pago_proximo + ]); + + // Enviar email al usuario + $user_email_sent = send_nextcloud_user_email($email_data); + + // Enviar email al administrador + $admin_email_sent = send_nextcloud_admin_email($email_data); + + // Log del resultado + nextcloud_log_info('Nextcloud TI plan processing completed', [ + 'user_id' => $user_id, + 'username' => $username, + 'level_name' => $level->name, + 'user_email_sent' => $user_email_sent, + 'admin_email_sent' => $admin_email_sent, + 'config_data' => $config_data + ]); + + return true; + + } catch (Exception $e) { + nextcloud_log_error('Exception in plan_nextcloud_ti', [ + 'user_id' => $user_id, + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + return false; + } +} + +/** + * Obtiene la configuración dinámica del usuario + */ +function get_nextcloud_user_config($user_id) { + $config_json = get_user_meta($user_id, 'nextcloud_config', true); + + if (empty($config_json)) { + nextcloud_log_info('No dynamic config found for user, using defaults', ['user_id' => $user_id]); + return [ + 'storage_space' => '1tb', + 'office_suite' => '20users', + 'payment_frequency' => 'monthly', + 'storage_display' => '1 Terabyte', + 'office_display' => '±20 usuários (CODE - Grátis)', + 'frequency_display' => 'Mensal' + ]; + } + + $config = json_decode($config_json, true); + if (json_last_error() !== JSON_ERROR_NONE) { + nextcloud_log_error('Invalid JSON in user config', [ + 'user_id' => $user_id, + 'json_error' => json_last_error_msg() + ]); + return null; + } + + // Enriquecer con información de display + $config['storage_display'] = get_storage_display_name($config['storage_space'] ?? '1tb'); + $config['office_display'] = get_office_display_name($config['office_suite'] ?? '20users'); + $config['frequency_display'] = get_frequency_display_name($config['payment_frequency'] ?? 'monthly'); + + return $config; +} + +/** + * Obtiene la fecha del próximo pago usando PMPro nativo + */ +function get_pmpro_next_payment_date($user_id, $level) { + // Usar función nativa de PMPro si está disponible + if (function_exists('pmpro_next_payment')) { + $next_payment = pmpro_next_payment($user_id); + if (!empty($next_payment)) { + return date('d/m/Y', $next_payment); + } + } + + // Fallback: calcular basado en el nivel y la última orden + if (class_exists('MemberOrder')) { + $last_order = new MemberOrder(); + $last_order->getLastMemberOrder($user_id, 'success'); + + if (!empty($last_order->timestamp)) { + $last_payment_timestamp = is_numeric($last_order->timestamp) + ? $last_order->timestamp + : strtotime($last_order->timestamp); + + // Calcular próximo pago basado en el ciclo + $cycle_seconds = get_cycle_seconds_from_level($level); + $next_payment_timestamp = $last_payment_timestamp + $cycle_seconds; + + return date('d/m/Y', $next_payment_timestamp); + } + } + + // Último fallback: basado en la fecha actual y ciclo del nivel + $cycle_seconds = get_cycle_seconds_from_level($level); + $next_payment_timestamp = current_time('timestamp') + $cycle_seconds; + + return date('d/m/Y', $next_payment_timestamp); +} + +/** + * Calcula segundos del ciclo basado en el nivel + */ +function get_cycle_seconds_from_level($level) { + if (empty($level->cycle_number) || empty($level->cycle_period)) { + return 30 * DAY_IN_SECONDS; // Default: 30 días + } + + $multipliers = [ + 'Day' => DAY_IN_SECONDS, + 'Week' => WEEK_IN_SECONDS, + 'Month' => 30 * DAY_IN_SECONDS, + 'Year' => YEAR_IN_SECONDS + ]; + + $multiplier = $multipliers[$level->cycle_period] ?? (30 * DAY_IN_SECONDS); + return $level->cycle_number * $multiplier; +} + +/** + * Prepara los datos para los emails + */ +function prepare_nextcloud_email_data($user, $level, $morder, $config_data, $additional_data) { + // Determinar mensajes basados en la frecuencia + $frequency_messages = get_frequency_messages($config_data['payment_frequency'] ?? 'monthly'); + + return [ + 'user' => $user, + 'level' => $level, + 'morder' => $morder, + 'config' => $config_data, + 'password' => $additional_data['password'], + 'fecha_pedido' => $additional_data['fecha_pedido'], + 'fecha_pago_proximo' => $additional_data['fecha_pago_proximo'], + 'monthly_message' => $frequency_messages['monthly_message'], + 'date_message' => $frequency_messages['date_message'] + ]; +} + +/** + * Obtiene mensajes según la frecuencia de pago + */ +function get_frequency_messages($payment_frequency) { + $messages = [ + 'monthly' => [ + 'monthly_message' => 'mensal ', + 'date_message' => 'Data do próximo pagamento: ' + ], + 'semiannual' => [ + 'monthly_message' => 'semestral ', + 'date_message' => 'Data da próxima cobrança semestral: ' + ], + 'annual' => [ + 'monthly_message' => 'anual ', + 'date_message' => 'Data da próxima cobrança anual: ' + ], + 'biennial' => [ + 'monthly_message' => 'bienal ', + 'date_message' => 'Data da próxima cobrança (em 2 anos): ' + ], + 'triennial' => [ + 'monthly_message' => 'trienal ', + 'date_message' => 'Data da próxima cobrança (em 3 anos): ' + ], + 'quadrennial' => [ + 'monthly_message' => 'quadrienal ', + 'date_message' => 'Data da próxima cobrança (em 4 anos): ' + ], + 'quinquennial' => [ + 'monthly_message' => 'quinquenal ', + 'date_message' => 'Data da próxima cobrança (em 5 anos): ' + ] + ]; + + return $messages[$payment_frequency] ?? $messages['monthly']; +} + +/** + * Envía email al usuario + */ +function send_nextcloud_user_email($data) { + $user = $data['user']; + $level = $data['level']; + $morder = $data['morder']; + $config = $data['config']; + + // Configuración del email + $brdrv_email = "cloud@" . basename(get_site_url()); + $mailto = "mailto:" . $brdrv_email; + + // Título del email + $subject = "Sua instância Nextcloud será criada"; + + // Construir mensaje + $message = "

Cloud Brasdrive

"; + $message .= "

Prezado(a) " . $user->display_name . " (" . $user->user_login . "),

"; + $message .= "

Parabéns! Seu pagamento foi confirmado e sua instância Nextcloud será criada em breve.

"; + + // Datos de acceso + $message .= "

Dados da sua conta admin do Nextcloud:

"; + $message .= "

Usuário: " . $user->user_login . "
"; + $message .= "Senha: " . $data['password'] . "

"; + + // Detalles del plan + $message .= "

Detalhes do seu plano:

"; + $message .= "

Plano: " . $level->name . "
"; + + // Agregar información de configuración dinámica si está disponible + if (!empty($config)) { + $message .= "Armazenamento: " . ($config['storage_display'] ?? 'N/A') . "
"; + $message .= "Suite Office: " . ($config['office_display'] ?? 'N/A') . "
"; + $message .= "Frequência: " . ($config['frequency_display'] ?? 'N/A') . "
"; + } + + $message .= "Data do pedido: " . $data['fecha_pedido'] . "
"; + $message .= "Valor " . $data['monthly_message'] . ": R$ " . number_format($morder->total, 2, ',', '.') . "
"; + $message .= $data['date_message'] . $data['fecha_pago_proximo'] . "

"; + + // Recomendações de segurança + $message .= "
"; + $message .= "

⚠️ Importante - Segurança:
"; + $message .= "Por segurança, recomendamos:

"; + $message .= "
    "; + $message .= "
  • Manter guardada a senha da instância Nextcloud em um local seguro
  • "; + $message .= "
  • Excluir este e-mail após salvar as informações
  • "; + $message .= "
  • Alterar sua senha nas Configurações pessoais de usuário da sua instância Nextcloud
  • "; + $message .= "
"; + + // Informações de contato + $message .= "

Se você tiver alguma dúvida, entre em contato conosco no e-mail: " . $brdrv_email . ".

"; + $message .= "

Atenciosamente,
Equipe Brasdrive

"; + + $headers = array('Content-Type: text/html; charset=UTF-8'); + + // Enviar email + $result = wp_mail($user->user_email, $subject, $message, $headers); + + if (!$result) { + nextcloud_log_error('Failed to send user email', [ + 'user_id' => $user->ID, + 'email' => $user->user_email + ]); + } + + return $result; +} + +/** + * Envía email al administrador + */ +function send_nextcloud_admin_email($data) { + $user = $data['user']; + $level = $data['level']; + $config = $data['config']; + + $to = get_option('admin_email'); + $subject = "Nova instância Nextcloud TI - " . $level->name; + + $admin_message = "

Nova instância Nextcloud TI contratada

"; + $admin_message .= "

Plano: " . $level->name . "
"; + $admin_message .= "Nome: " . $user->display_name . "
"; + $admin_message .= "Usuário: " . $user->user_login . "
"; + $admin_message .= "Email: " . $user->user_email . "
"; + $admin_message .= "Senha gerada: " . $data['password'] . "

"; + + // Agregar configuración dinámica + if (!empty($config)) { + $admin_message .= "

Configuração do plano:

"; + $admin_message .= "

Armazenamento: " . ($config['storage_display'] ?? 'N/A') . "
"; + $admin_message .= "Suite Office: " . ($config['office_display'] ?? 'N/A') . "
"; + $admin_message .= "Frequência: " . ($config['frequency_display'] ?? 'N/A') . "

"; + } + + $admin_message .= "

Data do pedido: " . $data['fecha_pedido'] . "
"; + $admin_message .= "Próximo pagamento: " . $data['fecha_pago_proximo'] . "

"; + + $headers = array('Content-Type: text/html; charset=UTF-8'); + + $result = wp_mail($to, $subject, $admin_message, $headers); + + if (!$result) { + nextcloud_log_error('Failed to send admin email', [ + 'admin_email' => $to, + 'user_id' => $user->ID + ]); + } + + return $result; +} + +// Funciones auxiliares para nombres de display +function get_storage_display_name($storage_space) { + $storage_options = [ + '1tb' => '1 Terabyte', '2tb' => '2 Terabytes', '3tb' => '3 Terabytes', + '4tb' => '4 Terabytes', '5tb' => '5 Terabytes', '6tb' => '6 Terabytes', + '7tb' => '7 Terabytes', '8tb' => '8 Terabytes', '9tb' => '9 Terabytes', + '10tb' => '10 Terabytes', '15tb' => '15 Terabytes', '20tb' => '20 Terabytes', + '30tb' => '30 Terabytes', '40tb' => '40 Terabytes', '50tb' => '50 Terabytes', + '60tb' => '60 Terabytes', '70tb' => '70 Terabytes', '80tb' => '80 Terabytes', + '90tb' => '90 Terabytes', '100tb' => '100 Terabytes', '200tb' => '200 Terabytes', + '300tb' => '300 Terabytes', '400tb' => '400 Terabytes', '500tb' => '500 Terabytes' + ]; + + + return $storage_options[$storage_space] ?? $storage_space; +} + +function get_office_display_name($office_suite) { + $office_options = [ + '20users' => '±20 usuários', + '30users' => '30 usuários', + '50users' => '50 usuários', + '80users' => '80 usuários', + '100users' => '100 usuários', + '150users' => '150 usuários', + '200users' => '200 usuários', + '300users' => '300 usuários', + '400users' => '400 usuários', + '500users' => '500 usuários' + ]; + + return $office_options[$office_suite] ?? $office_suite; +} + +function get_frequency_display_name($payment_frequency) { + $frequency_options = [ + 'monthly' => 'Mensal', + 'semiannual' => 'Semestral', + 'annual' => 'Anual', + 'biennial' => 'Bienal', + 'triennial' => 'Trienal', + 'quadrennial' => 'Quadrienal', + 'quinquennial' => 'Quinquenal' + ]; + + return $frequency_options[$payment_frequency] ?? $payment_frequency; +} diff --git a/storage/snippets/current-year.json b/storage/snippets/current-year.json new file mode 100644 index 0000000..0aee30b --- /dev/null +++ b/storage/snippets/current-year.json @@ -0,0 +1,7 @@ +{ + "description": "Current year shortcode", + "last_updated": "2025-08-15 02:55:04", + "active": true, + "hooks": [], + "load_priority": 10 +} \ No newline at end of file diff --git a/storage/snippets/current-year.php b/storage/snippets/current-year.php new file mode 100644 index 0000000..66c5dd0 --- /dev/null +++ b/storage/snippets/current-year.php @@ -0,0 +1,9 @@ +.login h1 a { visibility:hidden!important; }'; +} +add_action('login_head', 'custom_login_logo'); diff --git a/storage/snippets/get-nextcloud-storage-via-reports-api.json b/storage/snippets/get-nextcloud-storage-via-reports-api.json new file mode 100644 index 0000000..1e046bc --- /dev/null +++ b/storage/snippets/get-nextcloud-storage-via-reports-api.json @@ -0,0 +1,20 @@ +{ + "description": "Monitorear el espacio en Nextcloud y enviar reporte diario", + "last_updated": "2025-08-12 02:38:43", + "active": false, + "hooks": { + "wp": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + }, + "daily_nextcloud_storage_check": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + } + }, + "load_priority": 10 +} \ No newline at end of file diff --git a/storage/snippets/get-nextcloud-storage-via-reports-api.php b/storage/snippets/get-nextcloud-storage-via-reports-api.php new file mode 100644 index 0000000..b081285 --- /dev/null +++ b/storage/snippets/get-nextcloud-storage-via-reports-api.php @@ -0,0 +1,67 @@ + array( + 'OCS-APIRequest' => 'true', + 'Authorization' => 'Basic ' . base64_encode($nextcloud_api_admin . ':' . $nextcloud_api_pass), + ), + ); + + $response = wp_remote_get($endpoint, $args); + + if (!is_wp_error($response)) { + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (isset($body['ocs']['data']['nextcloud']['storage']['users'])) { + $total_used = $body['ocs']['data']['nextcloud']['storage']['used']; + $total_free = $body['ocs']['data']['nextcloud']['storage']['free']; + $total_space = $total_used + $total_free; + + return array( + 'used' => round($total_used / (1024 * 1024 * 1024), 2), // Convertir a GB + 'total' => round($total_space / (1024 * 1024 * 1024), 2) + ); + } + } + + // Preparar y enviar el email + $to = get_option('admin_email'); + $subject = 'Reporte de Almacenamiento Cloud Brasdrive'; + $message = sprintf( + $current_date, + 'Uso de almacenamiento en Cloud Brasdrive: %.2f GB de %d GB (%.1f%%) utilizados.', + $total_used, + $max_storage, + ($total_used / $max_storage) * 100 + ); + + wp_mail($to, $subject, $message); + + return false; +} + +// Programar el evento diario +function schedule_nextcloud_storage_check() { + if (!wp_next_scheduled('daily_nextcloud_storage_check')) { + wp_schedule_event(strtotime('01:00:00'), 'daily', 'daily_nextcloud_storage_check'); + } +} +add_action('wp', 'schedule_nextcloud_storage_check'); + +// Conectar la acción programada con nuestra función +add_action('daily_nextcloud_storage_check', 'get_nextcloud_storage_via_reports_api'); diff --git a/storage/snippets/nextcloud-banda-dynamic-pricing.json b/storage/snippets/nextcloud-banda-dynamic-pricing.json new file mode 100644 index 0000000..fcc8d18 --- /dev/null +++ b/storage/snippets/nextcloud-banda-dynamic-pricing.json @@ -0,0 +1,50 @@ +{ + "description": "PMPro Dynamic Pricing para el Plan Nextcloud Banda", + "last_updated": "2025-10-04 03:09:49", + "active": true, + "hooks": { + "admin_notices": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + }, + "plugins_loaded": { + "type": "action", + "priority": 25, + "accepted_args": 1, + "auto_detected": true + }, + "init": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + }, + "wp_loaded": { + "type": "action", + "priority": 5, + "accepted_args": 1, + "auto_detected": true + }, + "pmpro_checkout_level": { + "type": "filter", + "priority": 1, + "accepted_args": 1, + "auto_detected": true + }, + "pmpro_after_checkout": { + "type": "action", + "priority": 10, + "accepted_args": 2, + "auto_detected": true + }, + "pmpro_account_bullets_bottom": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + } + }, + "load_priority": 10 +} \ No newline at end of file diff --git a/storage/snippets/nextcloud-banda-dynamic-pricing.php b/storage/snippets/nextcloud-banda-dynamic-pricing.php new file mode 100644 index 0000000..577272f --- /dev/null +++ b/storage/snippets/nextcloud-banda-dynamic-pricing.php @@ -0,0 +1,2763 @@ + '1tb', + 'num_users' => 2, + 'payment_frequency' => 'monthly' + ]; + } + + // Validar y normalizar storage + $storage_space = sanitize_text_field($config_data['storage_space'] ?? '1tb'); + $valid_storage = ['1tb', '2tb', '3tb', '4tb', '5tb', '6tb', '7tb', '8tb', '9tb', '10tb', '15tb', '20tb']; + if (!in_array($storage_space, $valid_storage, true)) { + $storage_space = '1tb'; + } + + // Validar y normalizar usuarios (mínimo 2, máximo 20) + $num_users = max(2, min(20, intval($config_data['num_users'] ?? 2))); + + // Validar y normalizar frecuencia + $payment_frequency = sanitize_text_field($config_data['payment_frequency'] ?? 'monthly'); + $valid_frequencies = ['monthly', 'semiannual', 'annual', 'biennial', 'triennial', 'quadrennial', 'quinquennial']; + if (!in_array($payment_frequency, $valid_frequencies, true)) { + $payment_frequency = 'monthly'; + } + + return [ + 'storage_space' => $storage_space, + 'num_users' => $num_users, + 'payment_frequency' => $payment_frequency + ]; + } +} + +/** + * Obtiene configuración real del usuario desde múltiples fuentes + */ +function nextcloud_banda_get_user_real_config($user_id, $membership = null) { + $real_config = [ + 'storage_space' => null, + 'num_users' => null, + 'payment_frequency' => null, + 'final_amount' => null, + 'source' => 'none' + ]; + + // 1. Intentar obtener desde configuración guardada (JSON) + $config_json = get_user_meta($user_id, 'nextcloud_banda_config', true); + if (!empty($config_json)) { + $config = json_decode($config_json, true); + if (is_array($config) && json_last_error() === JSON_ERROR_NONE && !isset($config['auto_created'])) { + $real_config['storage_space'] = $config['storage_space'] ?? null; + $real_config['num_users'] = $config['num_users'] ?? null; + $real_config['payment_frequency'] = $config['payment_frequency'] ?? null; + $real_config['final_amount'] = $config['final_amount'] ?? null; + $real_config['source'] = 'saved_config'; + + nextcloud_banda_log_debug("Real config found from saved JSON for user {$user_id}", $real_config); + return $real_config; + } + } + + // 2. Intentar obtener desde campos personalizados de PMPro Register Helper + if (function_exists('pmprorh_getProfileField')) { + $storage_field = pmprorh_getProfileField('storage_space', $user_id); + $users_field = pmprorh_getProfileField('num_users', $user_id); + $frequency_field = pmprorh_getProfileField('payment_frequency', $user_id); + + if (!empty($storage_field) || !empty($users_field) || !empty($frequency_field)) { + $real_config['storage_space'] = $storage_field ?: null; + $real_config['num_users'] = $users_field ? intval($users_field) : null; + $real_config['payment_frequency'] = $frequency_field ?: null; + $real_config['source'] = 'profile_fields'; + + nextcloud_banda_log_debug("Real config found from profile fields for user {$user_id}", $real_config); + return $real_config; + } + } + + // 3. Intentar obtener desde user_meta directo + $storage_meta = get_user_meta($user_id, 'storage_space', true); + $users_meta = get_user_meta($user_id, 'num_users', true); + $frequency_meta = get_user_meta($user_id, 'payment_frequency', true); + + if (!empty($storage_meta) || !empty($users_meta) || !empty($frequency_meta)) { + $real_config['storage_space'] = $storage_meta ?: null; + $real_config['num_users'] = $users_meta ? intval($users_meta) : null; + $real_config['payment_frequency'] = $frequency_meta ?: null; + $real_config['source'] = 'user_meta'; + + nextcloud_banda_log_debug("Real config found from user meta for user {$user_id}", $real_config); + return $real_config; + } + + // 4. Intentar deducir desde información de membresía + if ($membership && !empty($membership->initial_payment)) { + $real_config['final_amount'] = (float)$membership->initial_payment; + $real_config['source'] = 'membership_deduction'; + + nextcloud_banda_log_debug("Config deduced from membership for user {$user_id}", $real_config); + } + + // Verificar si el usuario tiene una membresía activa + if (!pmpro_hasMembershipLevel($user_id)) { + // Forzar valores por defecto si no hay membresía activa + return [ + 'storage_space' => '1tb', + 'num_users' => 2, + 'payment_frequency' => 'monthly', + 'final_amount' => null, + 'source' => 'defaults_no_membership' + ]; + } + + nextcloud_banda_log_debug("No real config found for user {$user_id}, returning empty", $real_config); + return $real_config; +} + +/** + * Configuración centralizada - SINCRONIZADA + */ +function nextcloud_banda_get_config($key = null) { + static $config = null; + + if ($config === null) { + $config = [ + 'allowed_levels' => [2], // ID del nivel Nextcloud Banda + 'price_per_tb' => 70.00, // Precio por TB adicional + 'price_per_additional_user' => 10.00, // Precio por usuario adicional + 'base_users_included' => 2, // Usuarios incluidos en precio base + 'base_storage_included' => 1, // TB incluidos en precio base + 'base_price_default' => NEXTCLOUD_BANDA_BASE_PRICE, // CORREGIDO: Usar constante + 'min_users' => 2, + 'max_users' => 20, + 'min_storage' => 1, + 'max_storage' => 20, + 'frequency_multipliers' => [ + 'monthly' => 1.0, + 'semiannual' => 5.7, + 'annual' => 10.8, + 'biennial' => 20.4, + 'triennial' => 28.8, + 'quadrennial' => 36.0, + 'quinquennial' => 42.0 + ], + 'storage_options' => [ + '1tb' => '1 Terabyte', '2tb' => '2 Terabytes', '3tb' => '3 Terabytes', + '4tb' => '4 Terabytes', '5tb' => '5 Terabytes', '6tb' => '6 Terabytes', + '7tb' => '7 Terabytes', '8tb' => '8 Terabytes', '9tb' => '9 Terabytes', + '10tb' => '10 Terabytes', '15tb' => '15 Terabytes', '20tb' => '20 Terabytes' + ], + 'user_options' => [ + '2' => '2 usuários (incluídos)', + '3' => '3 usuários', + '4' => '4 usuários', + '5' => '5 usuários', + '6' => '6 usuários', + '7' => '7 usuários', + '8' => '8 usuários', + '9' => '9 usuários', + '10' => '10 usuários', + '15' => '15 usuários', + '20' => '20 usuários' + ] + ]; + } + + return $key ? ($config[$key] ?? null) : $config; +} + +// ==== +// SISTEMA DE LOGGING +// ==== + +function nextcloud_banda_log($level, $message, $context = []) { + static $log_level = null; + + if ($log_level === null) { + if (defined('WP_DEBUG') && WP_DEBUG) { + $log_level = 4; // DEBUG + } elseif (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + $log_level = 3; // INFO + } else { + $log_level = 1; // ERROR only + } + } + + $levels = [1 => 'ERROR', 2 => 'WARNING', 3 => 'INFO', 4 => 'DEBUG']; + + if ($level > $log_level) return; + + $log_message = sprintf( + '[PMPro Banda %s] %s', + $levels[$level], + $message + ); + + if (!empty($context)) { + $log_message .= ' | Context: ' . wp_json_encode($context, JSON_UNESCAPED_UNICODE); + } + + error_log($log_message); +} + +function nextcloud_banda_log_error($message, $context = []) { + nextcloud_banda_log(1, $message, $context); +} + +function nextcloud_banda_log_info($message, $context = []) { + nextcloud_banda_log(3, $message, $context); +} + +function nextcloud_banda_log_debug($message, $context = []) { + nextcloud_banda_log(4, $message, $context); +} + +// ==== +// SISTEMA DE CACHÉ +// ==== + +function nextcloud_banda_cache_get($key, $default = false) { + $cached = wp_cache_get($key, NEXTCLOUD_BANDA_CACHE_GROUP); + if ($cached !== false) { + nextcloud_banda_log_debug("Cache hit for key: {$key}"); + return $cached; + } + + nextcloud_banda_log_debug("Cache miss for key: {$key}"); + return $default; +} + +function nextcloud_banda_cache_set($key, $data, $expiry = NEXTCLOUD_BANDA_CACHE_EXPIRY) { + $result = wp_cache_set($key, $data, NEXTCLOUD_BANDA_CACHE_GROUP, $expiry); + nextcloud_banda_log_debug("Cache set for key: {$key}", ['success' => $result]); + return $result; +} + +function nextcloud_banda_invalidate_user_cache($user_id) { + $keys = [ + "banda_config_{$user_id}", + "pmpro_membership_{$user_id}", + "last_payment_date_{$user_id}", + "used_space_{$user_id}" + ]; + + foreach ($keys as $key) { + wp_cache_delete($key, NEXTCLOUD_BANDA_CACHE_GROUP); + } + + nextcloud_banda_log_info("User cache invalidated", ['user_id' => $user_id]); +} + +// ==== +// FUNCIONES DE API DE NEXTCLOUD - CORREGIDAS +// ==== + +function nextcloud_banda_api_get_group_used_space_mb($user_id) { + // CORREGIDO: Usar get_option en lugar de hardcoded + $site_url = get_option('siteurl'); + $nextcloud_api_url = 'https://cloud.' . parse_url($site_url, PHP_URL_HOST); + + // Obtener credenciales de variables de entorno + $nextcloud_api_admin = getenv('NEXTCLOUD_API_ADMIN'); + $nextcloud_api_pass = getenv('NEXTCLOUD_API_PASS'); + + // Verificar que las credenciales estén disponibles + if (empty($nextcloud_api_admin) || empty($nextcloud_api_pass)) { + nextcloud_banda_log_error('Las credenciales de la API de Nextcloud no están definidas en variables de entorno.'); + return false; + } + + // Obtener el nombre de usuario de WordPress, que se usará como el ID del grupo en Nextcloud + $wp_user = get_userdata($user_id); + if (!$wp_user) { + nextcloud_banda_log_error("No se pudo encontrar el usuario de WordPress con ID: {$user_id}"); + return false; + } + $group_id = 'banda-' . $user_id; + + // Argumentos base para las peticiones a la API + $api_args = [ + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode($nextcloud_api_admin . ':' . $nextcloud_api_pass), + 'OCS-APIRequest' => 'true', + 'Accept' => 'application/json', + ], + 'timeout' => 20, + ]; + + // Obtener la lista de usuarios del grupo + $users_url = sprintf('%s/ocs/v2.php/cloud/groups/%s/users', $nextcloud_api_url, urlencode($group_id)); + $response_users = wp_remote_get($users_url, $api_args); + + if (is_wp_error($response_users)) { + nextcloud_banda_log_error('Error en la conexión a la API de Nextcloud (obteniendo usuarios)', ['error' => $response_users->get_error_message()]); + return false; + } + + $status_code_users = wp_remote_retrieve_response_code($response_users); + if ($status_code_users !== 200) { + nextcloud_banda_log_error("La API de Nextcloud devolvió un error al obtener usuarios del grupo '{$group_id}'", ['status_code' => $status_code_users]); + return false; + } + + $users_body = wp_remote_retrieve_body($response_users); + $users_data = json_decode($users_body, true); + + if (empty($users_data['ocs']['data']['users'])) { + nextcloud_banda_log_info("El grupo '{$group_id}' no tiene usuarios o no existe en Nextcloud. Se devuelve 0MB."); + return 0.0; + } + + $nextcloud_user_ids = $users_data['ocs']['data']['users']; + $total_used_bytes = 0; + + // Obtener el espacio usado por cada usuario y sumarlo + foreach ($nextcloud_user_ids as $nc_user_id) { + $user_detail_url = sprintf('%s/ocs/v2.php/cloud/users/%s', $nextcloud_api_url, urlencode($nc_user_id)); + $response_user = wp_remote_get($user_detail_url, $api_args); + + if (is_wp_error($response_user) || wp_remote_retrieve_response_code($response_user) !== 200) { + nextcloud_banda_log_error("No se pudo obtener la información del usuario de Nextcloud: {$nc_user_id}"); + continue; + } + + $user_body = wp_remote_retrieve_body($response_user); + $user_data = json_decode($user_body, true); + + if (isset($user_data['ocs']['data']['quota']['used'])) { + $total_used_bytes += (int) $user_data['ocs']['data']['quota']['used']; + } + } + + // Convertir bytes a Megabytes + $total_used_mb = $total_used_bytes / (1024 * 1024); + + nextcloud_banda_log_debug("Cálculo de espacio finalizado para el grupo '{$group_id}'", [ + 'total_bytes' => $total_used_bytes, + 'total_mb' => $total_used_mb, + 'users_count' => count($nextcloud_user_ids) + ]); + + return $total_used_mb; +} + +// ==== +// VERIFICACIÓN DE DEPENDENCIAS +// ==== + +function nextcloud_banda_check_dependencies() { + static $dependencies_checked = false; + static $dependencies_ok = false; + + if ($dependencies_checked) { + return $dependencies_ok; + } + + $missing_plugins = []; + + if (!function_exists('pmprorh_add_registration_field')) { + $missing_plugins[] = 'PMPro Register Helper'; + nextcloud_banda_log_error('PMPro Register Helper functions not found'); + } + + if (!function_exists('pmpro_getOption')) { + $missing_plugins[] = 'Paid Memberships Pro'; + nextcloud_banda_log_error('PMPro core functions not found'); + } + + if (!class_exists('PMProRH_Field')) { + $missing_plugins[] = 'PMProRH_Field class'; + nextcloud_banda_log_error('PMProRH_Field class not available'); + } + + if (!empty($missing_plugins) && is_admin() && current_user_can('manage_options')) { + add_action('admin_notices', function() use ($missing_plugins) { + $plugins_list = implode(', ', $missing_plugins); + printf( + '

PMPro Banda Dynamic: Los siguientes plugins son requeridos: %s

', + esc_html($plugins_list) + ); + }); + } + + $dependencies_ok = empty($missing_plugins); + $dependencies_checked = true; + + return $dependencies_ok; +} + +// ==== +// FUNCIONES AUXILIARES +// ==== + +/** + * Convierte un valor de fecha a timestamp (segundos) de forma segura. + * Acepta: int (unix), string (Y-m-d H:i:s, Y-m-d, d/m/Y, ISO) o DateTimeInterface. + * Si no puede convertir, retorna time(). + * Además, recorta a un rango razonable (1970-01-01 a 2100-01-01). + */ +function nextcloud_banda_safe_ts($value) { + // int ya válido + if (is_int($value) && $value > 0 && $value < 4102444800) { // ~2100-01-01 + return $value; + } + // DateTime/Immutable + if ($value instanceof DateTimeInterface) { + $ts = $value->getTimestamp(); + return ($ts > 0 && $ts < 4102444800) ? $ts : time(); + } + // String + if (is_string($value) && $value !== '') { + // Intentar formatos explícitos primero + $formats = ['Y-m-d H:i:s', 'Y-m-d', 'd/m/Y', DateTimeInterface::ATOM, DATE_RFC3339]; + foreach ($formats as $fmt) { + $dt = DateTimeImmutable::createFromFormat($fmt, $value); + if ($dt instanceof DateTimeImmutable) { + $ts = $dt->getTimestamp(); + return ($ts > 0 && $ts < 4102444800) ? $ts : time(); + } + } + // Fallback general + $ts = strtotime($value); + if ($ts !== false && $ts > 0 && $ts < 4102444800) { + return $ts; + } + } + return time(); +} + +/** + * Obtiene info del ciclo actual basado estrictamente en cycle_number/cycle_period. + * Usa DateTimeImmutable y la zona horaria de WordPress para evitar desajustes. + * Devuelve timestamps (según timezone de WP) y next_payment_date como timestamp del final del ciclo actual (próximo cobro). + */ +function nextcloud_banda_get_next_payment_info($user_id) { + global $wpdb; + + nextcloud_banda_log_debug("=== GET NEXT PAYMENT INFO START ==="); + nextcloud_banda_log_debug("User ID: $user_id"); + + $current_level = pmpro_getMembershipLevelForUser($user_id); + if (empty($current_level) || empty($current_level->id)) { + nextcloud_banda_log_debug("ERROR: Usuario sin membresía activa"); + return false; + } + + $cycle_number = (int) ($current_level->cycle_number ?? 0); + $cycle_period = strtolower((string) ($current_level->cycle_period ?? '')); + nextcloud_banda_log_debug("Level Cycle: $cycle_number $cycle_period"); + + if ($cycle_number <= 0 || empty($cycle_period)) { + nextcloud_banda_log_debug("ERROR: Ciclo inválido en el nivel"); + return false; + } + + // Zona horaria de WP + $tz_string = get_option('timezone_string'); + if (!$tz_string) { + $gmt_offset = (float) get_option('gmt_offset', 0); + $tz_string = $gmt_offset >= 0 ? "UTC+$gmt_offset" : "UTC$gmt_offset"; + } + + try { + $tz = new DateTimeZone($tz_string); + } catch (Exception $e) { + $tz = new DateTimeZone('UTC'); + } + + // NOW en zona WP + $now_ts = current_time('timestamp'); + $now = (new DateTimeImmutable('@' . $now_ts))->setTimezone($tz); + + // Buscar última orden "success" + $last_order = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->pmpro_membership_orders} + WHERE user_id = %d AND status = 'success' + ORDER BY timestamp DESC + LIMIT 1", + $user_id + )); + + if (!$last_order) { + $last_order = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->pmpro_membership_orders} + WHERE user_id = %d AND status = 'cancelled' + ORDER BY timestamp DESC + LIMIT 1", + $user_id + )) ?: $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->pmpro_membership_orders} + WHERE user_id = %d AND status = 'refunded' + ORDER BY timestamp DESC + LIMIT 1", + $user_id + )); + } + + // Determinar cycle_start_anchor + if ($last_order && !empty($last_order->timestamp)) { + try { + $anchor_dt = new DateTimeImmutable($last_order->timestamp, $tz); + } catch (Exception $e) { + $anchor_dt = $now; + nextcloud_banda_log_debug("WARN: No se pudo parsear timestamp de orden, usando NOW"); + } + nextcloud_banda_log_debug("Última orden: ID {$last_order->id}, Timestamp: " . $anchor_dt->format('Y-m-d H:i:s')); + } elseif (!empty($current_level->startdate)) { + $start_ts = nextcloud_banda_safe_ts($current_level->startdate); + $anchor_dt = (new DateTimeImmutable('@' . $start_ts))->setTimezone($tz); + nextcloud_banda_log_debug("Usando startdate: " . $anchor_dt->format('Y-m-d H:i:s')); + } else { + $anchor_dt = $now; + nextcloud_banda_log_debug("No hay órdenes ni startdate, usando NOW como ancla"); + } + + // Normalizar a 00:00 para consistencia en cálculos diarios + $cycle_start_dt = $anchor_dt->setTime(0, 0, 0); + nextcloud_banda_log_debug("Cycle Start (normalizado): " . $cycle_start_dt->format('Y-m-d H:i:s')); + + // Construir intervalo del ciclo + switch ($cycle_period) { + case 'day': + case 'days': + $interval_spec = 'P' . $cycle_number . 'D'; + break; + case 'week': + case 'weeks': + $interval_spec = 'P' . ($cycle_number * 7) . 'D'; + break; + case 'month': + case 'months': + $interval_spec = 'P' . $cycle_number . 'M'; + break; + case 'year': + case 'years': + $interval_spec = 'P' . $cycle_number . 'Y'; + break; + default: + nextcloud_banda_log_debug("ERROR: Período desconocido: $cycle_period"); + return false; + } + + try { + $interval = new DateInterval($interval_spec); + } catch (Exception $e) { + nextcloud_banda_log_debug("ERROR: No se pudo crear DateInterval: $interval_spec"); + return false; + } + + // Calcular el ciclo actual de forma eficiente + $cycle_end_dt = $cycle_start_dt; + + // Para meses y años, usar cálculo más preciso + if ($cycle_period === 'month' || $cycle_period === 'months' || + $cycle_period === 'year' || $cycle_period === 'years') { + + // Estimar ciclos pasados + $estimate_cycles = 0; + if ($cycle_period === 'month' || $cycle_period === 'months') { + $diff = $cycle_start_dt->diff($now); + $months_total = $diff->y * 12 + $diff->m; + $estimate_cycles = (int) floor($months_total / $cycle_number); + } else { + $diff = $cycle_start_dt->diff($now); + $years_total = $diff->y; + $estimate_cycles = (int) floor($years_total / $cycle_number); + } + + // Avanzar por bloques grandes si hay muchos ciclos + if ($estimate_cycles > 0) { + try { + $block_interval_spec = ($cycle_period === 'month' || $cycle_period === 'months') + ? 'P' . ($estimate_cycles * $cycle_number) . 'M' + : 'P' . ($estimate_cycles * $cycle_number) . 'Y'; + $block_interval = new DateInterval($block_interval_spec); + $cycle_start_dt = $cycle_start_dt->add($block_interval); + $cycle_end_dt = $cycle_start_dt->add($interval); + } catch (Exception $e) { + // fallback a lógica simple + $cycle_end_dt = $cycle_start_dt->add($interval); + } + } else { + $cycle_end_dt = $cycle_start_dt->add($interval); + } + + // Ajustar hasta encontrar el ciclo correcto + $guard = 0; + while ($cycle_end_dt <= $now && $guard < 24) { + $cycle_start_dt = $cycle_end_dt; + $cycle_end_dt = $cycle_start_dt->add($interval); + $guard++; + } + + if ($guard >= 24) { + nextcloud_banda_log_debug("ERROR: Guardia excedida al ajustar ciclos"); + return false; + } + } else { + // Para días y semanas usar aritmética directa + $days_per_cycle = ($cycle_period === 'week' || $cycle_period === 'weeks') + ? $cycle_number * 7 : $cycle_number; + $days_since_start = (int) floor(($now->getTimestamp() - $cycle_start_dt->getTimestamp()) / DAY_IN_SECONDS); + $cycles_passed = ($days_since_start > 0) ? (int) floor($days_since_start / $days_per_cycle) : 0; + + if ($cycles_passed > 0) { + $cycle_start_dt = $cycle_start_dt->add(new DateInterval('P' . ($cycles_passed * $days_per_cycle) . 'D')); + } + $cycle_end_dt = $cycle_start_dt->add(new DateInterval('P' . $days_per_cycle . 'D')); + + // Ajuste final si necesario + if ($cycle_end_dt <= $now) { + $cycle_start_dt = $cycle_end_dt; + $cycle_end_dt = $cycle_start_dt->add(new DateInterval('P' . $days_per_cycle . 'D')); + } + } + + $cycle_start_ts = $cycle_start_dt->getTimestamp(); + $cycle_end_ts = $cycle_end_dt->getTimestamp(); + + // Calcular días totales y restantes + $total_seconds = max(1, $cycle_end_ts - $cycle_start_ts); + $remaining_seconds = max(0, $cycle_end_ts - $now->getTimestamp()); + $total_days = max(1, (int)round($total_seconds / DAY_IN_SECONDS)); + $days_remaining = max(0, (int)floor($remaining_seconds / DAY_IN_SECONDS)); + + if ($remaining_seconds > 0 && $days_remaining === 0) { + $days_remaining = 1; + } + + nextcloud_banda_log_debug("Cycle Start: " . $cycle_start_dt->format('Y-m-d H:i:s')); + nextcloud_banda_log_debug("Cycle End (Next Payment): " . $cycle_end_dt->format('Y-m-d H:i:s')); + nextcloud_banda_log_debug("Days remaining: $days_remaining, Total days: $total_days"); + nextcloud_banda_log_debug("=== GET NEXT PAYMENT INFO END ==="); + + return [ + 'next_payment_date' => $cycle_end_ts, + 'cycle_start' => $cycle_start_ts, + 'cycle_end' => $cycle_end_ts, + 'days_remaining' => $days_remaining, + 'total_days' => $total_days + ]; +} + +// ==== +// NUEVOS AUXILIARES +// ==== + +/** + * Obtiene información de prorrateo con cache + */ +function nextcloud_banda_get_cached_proration($user_id, $level_id, $storage, $users, $frequency) { + $cache_key = "proration_{$user_id}_{$level_id}_{$storage}_{$users}_{$frequency}"; + $cached = nextcloud_banda_cache_get($cache_key); + + if ($cached !== false) { + return $cached; + } + + $proration = nextcloud_banda_calculate_proration_core_aligned( + $user_id, $level_id, $storage, $users, $frequency + ); + + // Cache por 5 minutos + nextcloud_banda_cache_set($cache_key, $proration, 300); + + return $proration; +} + +/** + * Formatea valores monetarios de forma consistente + */ +function nextcloud_banda_format_currency($amount) { + return 'R$ ' . number_format((float)$amount, 2, ',', '.'); +} + +/** + * Formatea porcentajes de descuento + */ +function nextcloud_banda_format_discount($frequency) { + $discounts = [ + 'monthly' => 0, + 'semiannual' => 5, + 'annual' => 10, + 'biennial' => 15, + 'triennial' => 20, + 'quadrennial' => 25, + 'quinquennial' => 30 + ]; + + $discount = $discounts[$frequency] ?? 0; + return $discount > 0 ? "(-{$discount}%)" : ""; +} + +/** + * Valida configuración antes del checkout + */ +function nextcloud_banda_validate_checkout_config() { + $required_fields = ['storage_space', 'num_users', 'payment_frequency']; + + foreach ($required_fields as $field) { + if (!isset($_REQUEST[$field]) || empty($_REQUEST[$field])) { + return new WP_Error('missing_field', "Campo requerido: {$field}"); + } + } + + // Validar rangos + $num_users = (int)$_REQUEST['num_users']; + $config = nextcloud_banda_get_config(); + + if ($num_users < $config['min_users'] || $num_users > $config['max_users']) { + return new WP_Error('invalid_users', "Número de usuarios fuera de rango"); + } + + $storage_space = sanitize_text_field($_REQUEST['storage_space']); + $valid_storage = array_keys($config['storage_options']); + + if (!in_array($storage_space, $valid_storage, true)) { + return new WP_Error('invalid_storage', "Espacio de almacenamiento inválido"); + } + + $payment_frequency = strtolower(sanitize_text_field($_REQUEST['payment_frequency'])); + $valid_frequencies = array_keys($config['frequency_multipliers']); + + if (!in_array($payment_frequency, $valid_frequencies, true)) { + return new WP_Error('invalid_frequency', "Frecuencia de pago inválida"); + } + + return true; +} + +// ==== +// NUEVOS AUXILIARES - FIN +// ==== + +/** + * Suma un período usando DateTimeImmutable para evitar errores de strtotime/DST. + */ +function nextcloud_banda_add_period($timestamp, $number, $period) { + $number = (int)$number ?: 1; + $period = strtolower($period); + $dt = (new DateTimeImmutable('@' . nextcloud_banda_safe_ts($timestamp)))->setTimezone(wp_timezone()); + + switch ($period) { + case 'day': + case 'days': + $interval = new DateInterval('P' . $number . 'D'); + break; + case 'week': + case 'weeks': + $interval = new DateInterval('P' . $number . 'W'); + break; + case 'month': + case 'months': + $interval = new DateInterval('P' . $number . 'M'); + break; + case 'year': + case 'years': + $interval = new DateInterval('P' . $number . 'Y'); + break; + default: + $interval = new DateInterval('P' . $number . 'M'); + break; + } + $res = $dt->add($interval); + $ts = $res->getTimestamp(); + // Saneo de rango + if ($ts <= 0 || $ts >= 4102444800) { + $ts = time(); + } + return $ts; +} + +/** + * Resta un período usando DateTimeImmutable. + */ +function nextcloud_banda_sub_period($timestamp, $number, $period) { + $number = (int)$number ?: 1; + $period = strtolower($period); + $dt = (new DateTimeImmutable('@' . nextcloud_banda_safe_ts($timestamp)))->setTimezone(wp_timezone()); + + switch ($period) { + case 'day': + case 'days': + $interval = new DateInterval('P' . $number . 'D'); + break; + case 'week': + case 'weeks': + $interval = new DateInterval('P' . $number . 'W'); + break; + case 'month': + case 'months': + $interval = new DateInterval('P' . $number . 'M'); + break; + case 'year': + case 'years': + $interval = new DateInterval('P' . $number . 'Y'); + break; + default: + $interval = new DateInterval('P' . $number . 'M'); + break; + } + $res = $dt->sub($interval); + $ts = $res->getTimestamp(); + if ($ts <= 0 || $ts >= 4102444800) { + $ts = time(); + } + return $ts; +} + +function nextcloud_banda_get_used_space_tb($user_id) { + $cache_key = "used_space_{$user_id}"; + $cached = nextcloud_banda_cache_get($cache_key); + + if ($cached !== false) { + return $cached; + } + + // Llamada a la función de API real + $used_space_mb = nextcloud_banda_api_get_group_used_space_mb($user_id); + + // Si la llamada a la API falla, usar 0 como valor por defecto + if ($used_space_mb === false) { + nextcloud_banda_log_error("Fallo al obtener el espacio usado desde la API para user_id: {$user_id}. Se utilizará 0 como valor por defecto."); + $used_space_mb = 0; + } + + // Convierte el valor de MB a TB y redondea a 2 decimales + $used_space_tb = round($used_space_mb / 1024, 2); + + // Guarda el resultado en caché por 5 minutos + nextcloud_banda_cache_set($cache_key, $used_space_tb, 300); + + nextcloud_banda_log_debug("Espacio calculado desde API para user {$user_id}", [ + 'used_space_mb' => $used_space_mb, + 'used_space_tb' => $used_space_tb + ]); + + return $used_space_tb; +} + +function nextcloud_banda_get_current_level_id() { + static $cached_level_id = null; + + if ($cached_level_id !== null) { + return $cached_level_id; + } + + // CORREGIDO: Usar filter_input para mayor seguridad + $sources = [ + filter_input(INPUT_GET, 'level', FILTER_VALIDATE_INT), + filter_input(INPUT_GET, 'pmpro_level', FILTER_VALIDATE_INT), + filter_input(INPUT_POST, 'level', FILTER_VALIDATE_INT), + filter_input(INPUT_POST, 'pmpro_level', FILTER_VALIDATE_INT), + isset($_SESSION['pmpro_level']) ? (int)$_SESSION['pmpro_level'] : null, + isset($GLOBALS['pmpro_checkout_level']->id) ? (int)$GLOBALS['pmpro_checkout_level']->id : null, + isset($GLOBALS['pmpro_level']->id) ? (int)$GLOBALS['pmpro_level']->id : null, + ]; + + foreach ($sources as $source) { + if ($source > 0) { + $cached_level_id = $source; + nextcloud_banda_log_debug("Nivel detectado: {$source}"); + return $source; + } + } + + $cached_level_id = 0; + return 0; +} + +// ==== +// CAMPOS DINÁMICOS - CORREGIDOS +// ==== + +// Actualizar la función de campos dinámicos para manejar mejor el estado +function nextcloud_banda_add_dynamic_fields() { + $user_id = get_current_user_id(); + + // Verificar membresía activa usando next_payment_info + if ($user_id) { + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + $has_active_banda_membership = false; + + if (!empty($user_levels)) { + foreach ($user_levels as $level) { + if (in_array((int)$level->id, $allowed_levels, true)) { + // Verificar usando next_payment_info + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { + $has_active_banda_membership = true; + break; + } + } + } + } + + // Si no tiene membresía Banda activa, limpiar caché + if (!$has_active_banda_membership) { + nextcloud_banda_invalidate_user_cache($user_id); + } + } + + static $fields_added = false; + + if ($fields_added) { + return true; + } + + if (!nextcloud_banda_check_dependencies()) { + return false; + } + + $current_level_id = nextcloud_banda_get_current_level_id(); + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + + if (!in_array($current_level_id, $allowed_levels, true)) { + nextcloud_banda_log_info("Level {$current_level_id} not in allowed levels, skipping fields"); + return false; + } + + try { + $config = nextcloud_banda_get_config(); + $fields = []; + + // Obtener configuración real del usuario si existe + $real_config = []; + $current_banda_level = null; + + if ($user_id) { + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + + if (!empty($user_levels)) { + foreach ($user_levels as $lvl) { + if (in_array((int)$lvl->id, $allowed_levels, true)) { + $current_banda_level = $lvl; + break; + } + } + } + + // CORREGIDO: Usar la función base sin mejoras para obtener config real + if ($current_banda_level) { + $real_config = nextcloud_banda_get_user_real_config($user_id, $current_banda_level); + } + } + + // Definir opciones de frecuencia + $frequency_options = [ + 'monthly' => 'Mensal', + 'semiannual' => 'Semestral (-5%)', + 'annual' => 'Anual (-10%)', + 'biennial' => 'Bienal (-15%)', + 'triennial' => 'Trienal (-20%)', + 'quadrennial' => 'Quadrienal (-25%)', + 'quinquennial'=> 'Quinquenal (-30%)' + ]; + + // CORREGIDO: Definir valores por defecto verificando source + $default_storage = '1tb'; + $default_users = '2'; + $default_frequency = 'monthly'; + + // Si hay configuración real guardada, usarla + if (!empty($real_config) && + isset($real_config['source']) && + !in_array($real_config['source'], ['none', 'defaults_no_active_membership', 'membership_deduction'], true)) { + + $default_storage = $real_config['storage_space'] ?? '1tb'; + $default_users = isset($real_config['num_users']) ? strval($real_config['num_users']) : '2'; + $default_frequency = $real_config['payment_frequency'] ?? 'monthly'; + + nextcloud_banda_log_debug("Usando configuración real del usuario", [ + 'user_id' => $user_id, + 'source' => $real_config['source'], + 'storage' => $default_storage, + 'users' => $default_users, + 'frequency' => $default_frequency + ]); + } else { + nextcloud_banda_log_debug("Usando valores por defecto (sin config guardada)", [ + 'user_id' => $user_id, + 'source' => $real_config['source'] ?? 'empty' + ]); + } + + // Campo de almacenamiento + $fields[] = new PMProRH_Field( + 'storage_space', + 'select', + [ + 'label' => 'Espaço de armazenamento', + 'options' => $config['storage_options'], + 'profile' => true, + 'required' => false, + 'memberslistcsv' => true, + 'addmember' => true, + 'location' => 'after_level', + 'default' => $default_storage + ] + ); + + // Campo de número de usuários + $fields[] = new PMProRH_Field( + 'num_users', + 'select', + [ + 'label' => 'Número de usuários', + 'options' => $config['user_options'], + 'profile' => true, + 'required' => false, + 'memberslistcsv' => true, + 'addmember' => true, + 'location' => 'after_level', + 'default' => $default_users + ] + ); + + // Campo de ciclo + $fields[] = new PMProRH_Field( + 'payment_frequency', + 'select', + [ + 'label' => 'Ciclo de pagamento', + 'options' => $frequency_options, + 'profile' => true, + 'required' => false, + 'memberslistcsv' => true, + 'addmember' => true, + 'location' => 'after_level', + 'default' => $default_frequency + ] + ); + + // Campo de precio total + $fields[] = new PMProRH_Field( + 'total_price_display', + 'text', + [ + 'label' => 'Preço total', + 'profile' => false, + 'required' => false, + 'memberslistcsv' => false, + 'addmember' => false, + 'readonly' => true, + 'location' => 'after_level', + 'showrequired' => false, + 'divclass' => 'pmpro_checkout-field-price-display', + 'default' => 'R$ ' . number_format(NEXTCLOUD_BANDA_BASE_PRICE, 2, ',', '.') + ] + ); + + // Añadir campos + foreach($fields as $field) { + pmprorh_add_registration_field('Configuração do plano', $field); + } + + $fields_added = true; + + nextcloud_banda_log_info("Dynamic fields added successfully", [ + 'level_id' => $current_level_id, + 'fields_count' => count($fields), + 'base_price' => NEXTCLOUD_BANDA_BASE_PRICE, + 'default_values' => [ + 'storage' => $default_storage, + 'users' => $default_users, + 'frequency' => $default_frequency + ] + ]); + + return true; + + } catch (Exception $e) { + nextcloud_banda_log_error('Exception adding dynamic fields', [ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + return false; + } +} + +// ==== +// CÁLCULOS DE PRECIO - CORREGIDOS +// ==== + +function nextcloud_banda_calculate_pricing($storage_space, $num_users, $payment_frequency, $base_price) { + if (empty($storage_space) || empty($num_users) || empty($payment_frequency)) { + nextcloud_banda_log_error('Missing parameters for price calculation'); + return $base_price ?: NEXTCLOUD_BANDA_BASE_PRICE; // CORREGIDO: Usar constante + } + + // Verificar caché + $cache_key = "pricing_{$storage_space}_{$num_users}_{$payment_frequency}_{$base_price}"; + $cached_price = nextcloud_banda_cache_get($cache_key); + if ($cached_price !== false) { + return $cached_price; + } + + $config = nextcloud_banda_get_config(); + $price_per_tb = $config['price_per_tb']; + $price_per_user = $config['price_per_additional_user']; + $base_users_included = $config['base_users_included']; + $base_storage_included = $config['base_storage_included']; + + // CORREGIDO: Asegurar precio base válido + if ($base_price <= 0) { + $base_price = NEXTCLOUD_BANDA_BASE_PRICE; + } + + // Calcular precio de almacenamiento (1TB incluido en base_price) + $storage_tb = (int)str_replace('tb', '', $storage_space); + $additional_tb = max(0, $storage_tb - $base_storage_included); + $storage_price = $base_price + ($price_per_tb * $additional_tb); + + // Calcular precio por usuarios (2 usuarios incluidos en base_price) + $additional_users = max(0, (int)$num_users - $base_users_included); + $user_price = $price_per_user * $additional_users; + + // Precio combinado + $combined_price = $storage_price + $user_price; + + // Aplicar multiplicador de frecuencia + $multipliers = $config['frequency_multipliers']; + $frequency_multiplier = $multipliers[$payment_frequency] ?? 1.0; + + // Calcular precio total + $total_price = ceil($combined_price * $frequency_multiplier); + + // Guardar en caché + nextcloud_banda_cache_set($cache_key, $total_price, 300); + + nextcloud_banda_log_debug('Price calculated', [ + 'storage_space' => $storage_space, + 'storage_tb' => $storage_tb, + 'additional_tb' => $additional_tb, + 'num_users' => $num_users, + 'additional_users' => $additional_users, + 'payment_frequency' => $payment_frequency, + 'base_price' => $base_price, + 'storage_price' => $storage_price, + 'user_price' => $user_price, + 'combined_price' => $combined_price, + 'total_price' => $total_price + ]); + + return $total_price; +} + +function nextcloud_banda_configure_billing_period($level, $payment_frequency, $total_price) { + if (empty($level) || !is_object($level)) { + nextcloud_banda_log_error('Invalid level object provided'); + return $level; + } + + // Mapa de frecuencias a ciclo PMPro + $billing_cycles = [ + 'monthly' => ['number' => 1, 'period' => 'Month'], + 'semiannual' => ['number' => 6, 'period' => 'Month'], + 'annual' => ['number' => 12, 'period' => 'Month'], + 'biennial' => ['number' => 24, 'period' => 'Month'], + 'triennial' => ['number' => 36, 'period' => 'Month'], + 'quadrennial' => ['number' => 48, 'period' => 'Month'], + 'quinquennial' => ['number' => 60, 'period' => 'Month'], + ]; + $freq = strtolower($payment_frequency ?: 'monthly'); + $cycle = $billing_cycles[$freq] ?? $billing_cycles['monthly']; + + // Establecer ciclo real en el level (PMPro/gateway usarán estos valores) + $level->cycle_number = (int)$cycle['number']; + $level->cycle_period = $cycle['period']; + $level->billing_amount = (float)$total_price; + $level->initial_payment = (float)$total_price; + $level->trial_amount = 0; + $level->trial_limit = 0; + $level->recurring = true; + + return $level; +} + +// ==== +// HOOK PRINCIPAL DE MODIFICACIÓN DE PRECIO (MODIFICADO CON PRORRATEO) +// ==== + +/** + * Ajusta dinámicamente el precio del nivel seleccionado en el checkout + * con base en la configuración de Nextcloud Banda y adjunta la información + * de prorrateo para su consumo posterior en la interfaz. + * + * @param stdClass $level + * @param int|null $user_id + * + * @return stdClass + */ +function nextcloud_banda_modify_level_pricing($level, $user_id = null) { + if (empty($level) || empty($level->id)) { + return $level; + } + + if (empty($user_id)) { + $user_id = get_current_user_id(); + } + + if (empty($user_id)) { + nextcloud_banda_log_debug('modify_level_pricing: sin user_id, devolviendo nivel sin cambios.', []); + return $level; + } + + nextcloud_banda_log_debug('=== MODIFY LEVEL PRICING START ===', [ + 'user_id' => $user_id, + 'level_id' => $level->id, + 'level_name' => $level->name ?? '', + ]); + + // Obtener configuración del usuario + $user_config = nextcloud_banda_get_user_config($user_id); + + // Valores por defecto + $config = nextcloud_banda_get_config(); + $default_storage = '1tb'; + $default_users = 2; + $default_frequency = 'monthly'; + + // Obtener valores del request o configuración guardada + $storage_space = isset($_REQUEST['storage_space']) ? + sanitize_text_field($_REQUEST['storage_space']) : + ($user_config['storage'] ? $user_config['storage'] . 'tb' : $default_storage); + + $num_users = isset($_REQUEST['num_users']) ? + (int) $_REQUEST['num_users'] : + ($user_config['users'] ?: $default_users); + + $payment_frequency = isset($_REQUEST['payment_frequency']) ? + strtolower(sanitize_text_field($_REQUEST['payment_frequency'])) : + ($user_config['frequency'] ?: $default_frequency); + + // Validar frecuencia + $multipliers = $config['frequency_multipliers'] ?? ['monthly' => 1.0]; + if (!isset($multipliers[$payment_frequency])) { + $payment_frequency = 'monthly'; + } + + // Obtener precio base de referencia + $base_price_reference = isset($level->initial_payment) ? + (float) $level->initial_payment : + NEXTCLOUD_BANDA_BASE_PRICE; + + // Calcular precio + $calculated_price = nextcloud_banda_calculate_pricing( + $storage_space, + $num_users, + $payment_frequency, + $base_price_reference + ); + + // Aplicar el precio calculado al nivel + $level->initial_payment = round((float) $calculated_price, 2); + $level->billing_amount = round((float) $calculated_price, 2); + $level->recurring = true; + $level->trial_amount = 0; + $level->trial_limit = 0; + + // Configurar el ciclo de facturación + $level = nextcloud_banda_configure_billing_period($level, $payment_frequency, $calculated_price); + + // Calcular y adjuntar información de prorrateo solo para usuarios con membresía activa + if ($user_id && pmpro_hasMembershipLevel($level->id, $user_id)) { + $proration = nextcloud_banda_calculate_proration_core_aligned( + $user_id, + $level->id, + $storage_space, + $num_users, + $payment_frequency + ); + + // Adjuntar información de prorrateo al nivel + $level->nextcloud_banda_proration = $proration; + + nextcloud_banda_log_debug('Proration info attached to level', [ + 'user_id' => $user_id, + 'proration_attached' => !empty($proration), + 'prorated_amount' => $proration['prorated_amount'] ?? 0 + ]); + } + + nextcloud_banda_log_debug('=== MODIFY LEVEL PRICING END ===', [ + 'calculated_price' => $level->initial_payment, + 'storage_space' => $storage_space, + 'num_users' => $num_users, + 'payment_frequency' => $payment_frequency + ]); + + return $level; +} + +// ==== +// >>> PRORATION SYSTEM: begin +// ==== + +/** + * Obtiene la configuración actual del usuario de forma precisa + */ +function nextcloud_banda_get_user_config($user_id) { + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + + $current_banda_level = null; + if (!empty($user_levels)) { + foreach ($user_levels as $level) { + if (in_array((int)$level->id, $allowed_levels, true)) { + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { + $current_banda_level = $level; + break; + } + } + } + } + + // Si no hay membresía activa, devolver valores por defecto + if (!$current_banda_level) { + return [ + 'storage' => 1, + 'users' => 2, + 'frequency' => 'monthly' + ]; + } + + // Obtener configuración real usando la función mejorada + $real_config = nextcloud_banda_get_user_real_config_improved($user_id, $current_banda_level); + + // Convertir storage_space a número + $storage_tb = 1; + if (!empty($real_config['storage_space'])) { + $storage_tb = (int)str_replace('tb', '', strtolower($real_config['storage_space'])); + } + + return [ + 'storage' => $storage_tb, + 'users' => isset($real_config['num_users']) ? (int)$real_config['num_users'] : 2, + 'frequency' => $real_config['payment_frequency'] ?? 'monthly' + ]; +} + +/** + * Calcula el precio para una configuración específica + */ +function nextcloud_banda_calculate_price($level_id, $storage_tb, $num_users) { + $config = nextcloud_banda_get_config(); + + $base_price = $config['base_price_default']; + $price_per_tb = $config['price_per_tb']; + $price_per_user = $config['price_per_additional_user']; + $base_storage_included = $config['base_storage_included']; + $base_users_included = $config['base_users_included']; + + // Calcular precio de almacenamiento + $additional_tb = max(0, $storage_tb - $base_storage_included); + $storage_price = $base_price + ($price_per_tb * $additional_tb); + + // Calcular precio por usuarios + $additional_users = max(0, $num_users - $base_users_included); + $user_price = $price_per_user * $additional_users; + + // Precio total (sin multiplicador de frecuencia) + $total_price = $storage_price + $user_price; + + nextcloud_banda_log_debug('Price calculated for config', [ + 'level_id' => $level_id, + 'storage_tb' => $storage_tb, + 'num_users' => $num_users, + 'additional_tb' => $additional_tb, + 'additional_users' => $additional_users, + 'total_price' => $total_price + ]); + + return $total_price; +} + +/** + * Calcula el prorrateo basado en el ciclo real del nivel PMPro + * CORREGIDO: Cálculo matemático preciso de prorrateo + */ +function nextcloud_banda_calculate_proration_core_aligned($user_id, $new_level_id, $new_storage, $new_users, $new_frequency = 'monthly') { + nextcloud_banda_log_debug("=== PRORATION CORE-ALIGNED START ===", [ + 'user_id' => $user_id, + 'new_level_id' => $new_level_id, + 'new_storage' => $new_storage, + 'new_users' => $new_users, + 'new_frequency' => $new_frequency + ]); + + $current_level = pmpro_getMembershipLevelForUser($user_id); + if (empty($current_level) || empty($current_level->id)) { + nextcloud_banda_log_debug("Usuario sin membresía activa, proration = 0"); + return [ + 'prorated_amount' => 0, + 'days_remaining' => 0, + 'total_days' => 1, + 'next_payment_date' => '', + 'message' => 'No active membership' + ]; + } + + $config = nextcloud_banda_get_config(); + $multipliers = $config['frequency_multipliers'] ?? ['monthly' => 1.0]; + + // Normalizar storage + $normalize_storage = function($value) { + if (is_numeric($value)) { + return max(1, (int)$value) . 'tb'; + } + $value = strtolower(trim((string)$value)); + if ($value === '') { + return '1tb'; + } + if (strpos($value, 'tb') === false && strpos($value, 'gb') === false) { + $digits = preg_replace('/[^0-9]/', '', $value); + return $digits !== '' ? $digits . 'tb' : '1tb'; + } + return $value; + }; + + $extract_tb = function($slug) { + $slug = strtolower((string)$slug); + if (strpos($slug, 'gb') !== false) { + $numeric = (float)preg_replace('/[^0-9.]/', '', $slug); + return max(1, (int)ceil($numeric / 1024)); + } + $numeric = (int)preg_replace('/[^0-9]/', '', $slug); + return max(1, $numeric); + }; + + // Validar y normalizar frecuencias + $new_frequency = strtolower($new_frequency ?: 'monthly'); + if (!isset($multipliers[$new_frequency])) { + $new_frequency = 'monthly'; + } + + // Obtener configuraciones actuales - MEJORADO + $user_config_simple = nextcloud_banda_get_user_config($user_id); + $current_config = nextcloud_banda_get_user_real_config_improved($user_id, $current_level); + + // Extraer y normalizar configuración actual - MÁS ROBUSTO + $current_storage_slug = $normalize_storage( + $current_config['storage_space'] ?? ($user_config_simple['storage'] . 'tb' ?? '1tb') + ); + $current_users = (int)($current_config['num_users'] ?? $user_config_simple['users'] ?? 2); + $current_users = max(2, $current_users); // Mínimo 2 usuarios + + $current_frequency = strtolower( + $current_config['payment_frequency'] ?? ($user_config_simple['frequency'] ?? 'monthly') + ); + if (!isset($multipliers[$current_frequency])) { + $current_frequency = 'monthly'; + } + + // VALIDACIÓN CRÍTICA: Asegurar que tenemos los datos correctos + nextcloud_banda_log_debug("CONFIGURACIÓN ACTUAL DETECTADA", [ + 'from_real_config' => [ + 'storage_space' => $current_config['storage_space'] ?? 'NOT_SET', + 'num_users' => $current_config['num_users'] ?? 'NOT_SET', + 'payment_frequency' => $current_config['payment_frequency'] ?? 'NOT_SET', + 'final_amount' => $current_config['final_amount'] ?? 'NOT_SET', + 'source' => $current_config['source'] ?? 'NOT_SET' + ], + 'from_simple_config' => $user_config_simple, + 'normalized_values' => [ + 'storage_slug' => $current_storage_slug, + 'users' => $current_users, + 'frequency' => $current_frequency + ] + ]); + + // Obtener precios base + $base_price_reference = NEXTCLOUD_BANDA_BASE_PRICE; + $level_obj = pmpro_getLevel($new_level_id); + if ($level_obj && !empty($level_obj->initial_payment)) { + $base_price_reference = (float)$level_obj->initial_payment; + } + + // Normalizar nuevas configuraciones + $new_storage_slug = $normalize_storage($new_storage); + $new_users = max(2, (int)$new_users); + $new_frequency = strtolower($new_frequency ?: 'monthly'); + if (!isset($multipliers[$new_frequency])) { + $new_frequency = 'monthly'; + } + + // Calcular precios actuales y nuevos - MEJORADO + $current_cycle_price = null; + + // PRIORIDAD 1: Usar precio final guardado + if (!empty($current_config['final_amount']) && $current_config['final_amount'] > 0) { + $current_cycle_price = round((float)$current_config['final_amount'], 2); + nextcloud_banda_log_debug("Usando precio guardado de configuración", [ + 'precio_guardado' => $current_cycle_price, + 'source' => $current_config['source'] + ]); + } + + // PRIORIDAD 2: Usar initial_payment del nivel actual + if (($current_cycle_price === null || $current_cycle_price <= 0) && !empty($current_level->initial_payment)) { + $current_cycle_price = round((float)$current_level->initial_payment, 2); + nextcloud_banda_log_debug("Usando initial_payment del nivel", [ + 'precio_nivel' => $current_cycle_price + ]); + } + + // PRIORIDAD 3: Calcular basado en configuración + if ($current_cycle_price === null || $current_cycle_price <= 0) { + $current_cycle_price = (float)nextcloud_banda_calculate_pricing( + $current_storage_slug, + $current_users, + $current_frequency, + $base_price_reference + ); + nextcloud_banda_log_debug("Calculando precio basado en configuración", [ + 'precio_calculado' => $current_cycle_price, + 'storage' => $current_storage_slug, + 'users' => $current_users, + 'frequency' => $current_frequency, + 'base_price' => $base_price_reference + ]); + } + + // Asegurar que tenemos un precio válido + if ($current_cycle_price === null || $current_cycle_price <= 0) { + $current_cycle_price = $base_price_reference; + nextcloud_banda_log_debug("USANDO PRECIO BASE POR DEFECTO", [ + 'precio_base' => $current_cycle_price + ]); + } + + $new_cycle_price = (float)nextcloud_banda_calculate_pricing( + $new_storage_slug, + $new_users, + $new_frequency, + $base_price_reference + ); + + // VALIDACIÓN FINAL ANTES DE PRORRATEO + nextcloud_banda_log_debug("VALIDACIÓN PRE-PRORRATEO", [ + 'current_cycle_price' => $current_cycle_price, + 'new_cycle_price' => $new_cycle_price, + 'price_diff' => $new_cycle_price - $current_cycle_price, + 'current_config' => [ + 'storage' => $current_storage_slug, + 'users' => $current_users, + 'frequency' => $current_frequency + ], + 'new_config' => [ + 'storage' => $new_storage_slug, + 'users' => $new_users, + 'frequency' => $new_frequency + ] + ]); + + // Verificar que los precios sean válidos + if ($current_cycle_price <= 0) { + nextcloud_banda_log_error("PRECIO ACTUAL INVÁLIDO", [ + 'precio' => $current_cycle_price, + 'configuracion' => $current_config + ]); + return [ + 'prorated_amount' => 0, + 'days_remaining' => 0, + 'total_days' => 1, + 'next_payment_date' => '', + 'message' => 'Invalid current price', + 'debug_info' => [ + 'current_cycle_price' => $current_cycle_price, + 'current_config_source' => $current_config['source'] ?? 'unknown' + ] + ]; + } + + // CÁLCULO PRECISO DE PRORRATEO - CON VALIDACIONES + $price_diff = round($new_cycle_price - $current_cycle_price, 2); + + nextcloud_banda_log_debug("ANÁLISIS DE DIFERENCIA DE PRECIO", [ + 'precio_nuevo' => $new_cycle_price, + 'precio_actual' => $current_cycle_price, + 'diferencia' => $price_diff + ]); + + // Si no hay diferencia de precio o es downgrade significativo, manejar adecuadamente + if ($price_diff <= 0) { + nextcloud_banda_log_debug("SIN DIFERENCIA O DOWNGRADE", [ + 'price_diff' => $price_diff, + 'current_price' => $current_cycle_price, + 'new_price' => $new_cycle_price, + 'mensaje' => $price_diff <= 0 ? 'Downgrade o mismo precio' : 'Upgrade positivo' + ]); + + // Para downgrades, podemos devolver 0 o manejar según política de negocio + return [ + 'prorated_amount' => 0, + 'days_remaining' => $days_remaining ?? 0, + 'total_days' => $total_days ?? 1, + 'next_payment_date' => '', + 'message' => 'Downgrade or same price', + 'current_price' => round($current_cycle_price, 2), + 'new_price' => round($new_cycle_price, 2), + 'price_diff' => $price_diff, + 'current_frequency' => $current_frequency, + 'new_frequency' => $new_frequency + ]; + } + + // Obtener información del ciclo de pago + $payment_info = nextcloud_banda_get_next_payment_info($user_id); + if (!$payment_info || empty($payment_info['next_payment_date'])) { + nextcloud_banda_log_error("No se pudo obtener next_payment_date"); + return [ + 'prorated_amount' => 0, + 'days_remaining' => 0, + 'total_days' => 1, + 'next_payment_date' => '', + 'message' => 'Could not determine next payment date' + ]; + } + + // Calcular días restantes y totales + $cycle_start_ts = (int)$payment_info['cycle_start']; + $cycle_end_ts = (int)$payment_info['cycle_end']; + $now = current_time('timestamp'); + + $total_seconds = max(1, $cycle_end_ts - $cycle_start_ts); + $remaining_seconds = max(0, $cycle_end_ts - $now); + $fraction_remaining = $total_seconds > 0 ? $remaining_seconds / $total_seconds : 0; + + $total_days = max(1, (int)round($total_seconds / DAY_IN_SECONDS)); + $days_remaining = max(0, (int)floor($remaining_seconds / DAY_IN_SECONDS)); + if ($remaining_seconds > 0 && $days_remaining === 0) { + $days_remaining = 1; + } + + // CÁLCULO MATEMÁTICO PRECISO DE PRORRATEO + // Basado en tu ejemplo: R$ 1.055,00 - (R$ 1.055,00 × 180/182) = Crédito + // Nuevo precio: R$ 2.862,00 - Crédito = Monto a pagar + + $daily_rate = $current_cycle_price / $total_days; + $current_credit_precise = $daily_rate * $days_remaining; + $prorated_amount_precise = $new_cycle_price - $current_credit_precise; + + // Redondeo cuidadoso para evitar errores de centavos + $current_credit = round($current_credit_precise, 2); + $prorated_amount = round($prorated_amount_precise, 2); + + // Asegurar que no sea negativo + $prorated_amount = max(0, $prorated_amount); + + // Cálculo adicional para mostrar detalles + $prorated_new = round($new_cycle_price * $fraction_remaining, 2); + + nextcloud_banda_log_debug("CÁLCULO PRORRATEO DETALLADO", [ + 'metodo' => 'Precio nuevo - (Precio actual × fracción restante)', + 'precio_nuevo' => $new_cycle_price, + 'precio_actual' => $current_cycle_price, + 'fraccion_restante' => $fraction_remaining, + 'dias_restantes' => $days_remaining, + 'dias_totales' => $total_days, + 'tasa_diaria' => $daily_rate, + 'credito_actual_preciso' => $current_credit_precise, + 'credito_actual_redondeado' => $current_credit, + 'monto_prorrateado_preciso' => $prorated_amount_precise, + 'monto_prorrateado_final' => $prorated_amount + ]); + + // Asegurar consistencia en los cálculos + if ($days_remaining > 0 && $total_days > 0) { + $fraction_check = $days_remaining / $total_days; + nextcloud_banda_log_debug("VERIFICACIÓN DE FRACCIÓN", [ + 'calculada' => $fraction_remaining, + 'verificada' => $fraction_check, + 'diferencia' => abs($fraction_remaining - $fraction_check) + ]); + } + + // Asegurar que el crédito no exceda el precio actual + $current_credit = min($current_credit, $current_cycle_price); + $prorated_amount = max(0, $new_cycle_price - $current_credit); + + nextcloud_banda_log_debug("VALORES FINALES AJUSTADOS", [ + 'credito_final' => $current_credit, + 'monto_a_pagar' => $prorated_amount, + 'verificacion' => $new_cycle_price - $current_credit + ]); + + // Generar etiquetas para las frecuencias + $frequency_labels = [ + 'monthly' => 'Mensal', + 'semiannual' => 'Semestral', + 'annual' => 'Anual', + 'biennial' => 'Bienal', + 'triennial' => 'Trienal', + 'quadrennial' => 'Quadrienal', + 'quinquennial' => 'Quinquenal' + ]; + + $current_cycle_label = $frequency_labels[$current_frequency] ?? ucfirst($current_frequency); + $new_cycle_label = $frequency_labels[$new_frequency] ?? ucfirst($new_frequency); + + $result = [ + 'prorated_amount' => $prorated_amount, + 'prorated_new_amount' => $prorated_new, + 'current_prorated_amount' => $current_credit, + 'price_diff' => $price_diff, + 'fraction_remaining' => $fraction_remaining, + 'remaining_seconds' => $remaining_seconds, + 'total_seconds' => $total_seconds, + 'days_remaining' => $days_remaining, + 'total_days' => $total_days, + 'next_payment_date' => date('Y-m-d', (int)$payment_info['next_payment_date']), + 'current_price' => round($current_cycle_price, 2), + 'new_price' => round($new_cycle_price, 2), + 'current_frequency' => $current_frequency, + 'current_cycle_label' => $current_cycle_label, + 'new_frequency' => $new_frequency, + 'new_cycle_label' => $new_cycle_label, + 'message' => 'Success', + 'debug_info' => [ + 'calculation_method' => 'new_price - (current_price * fraction_remaining)', + 'daily_rate' => round($daily_rate, 4), + 'credit_calculation' => "{$current_cycle_price} × {$fraction_remaining} = {$current_credit}", + 'final_calculation' => "{$new_cycle_price} - {$current_credit} = {$prorated_amount}", + 'days_ratio' => "{$days_remaining}/{$total_days}" + ] + ]; + + nextcloud_banda_log_debug("RESULTADO FINAL DE PRORRATEO", $result); + + return $result; +} + +/** + * Determina si el usuario está actualizando su plan (considera membresía activa vía próxima fecha de pago). + */ +function nextcloud_banda_is_plan_upgrade($user_id, $new_storage, $new_users, $new_frequency) { + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + + $current_banda_level = null; + if (!empty($user_levels)) { + foreach ($user_levels as $level) { + if (in_array((int)$level->id, $allowed_levels, true)) { + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { + $current_banda_level = $level; + break; + } + } + } + } + + if (!$current_banda_level) { + nextcloud_banda_log_debug('No upgrade - no active membership by next payment', ['user_id' => $user_id]); + return false; + } + + $current_config = nextcloud_banda_get_user_real_config_improved($user_id, $current_banda_level); + + $current_storage = $current_config['storage_space'] ?: '1tb'; + $current_users = $current_config['num_users'] ?: 2; + $current_frequency = $current_config['payment_frequency'] ?: 'monthly'; + + $current_storage_tb = (int)str_replace('tb', '', $current_storage); + $new_storage_tb = (int)str_replace('tb', '', $new_storage); + + $frequency_order = [ + 'monthly' => 1, + 'semiannual' => 2, + 'annual' => 3, + 'biennial' => 4, + 'triennial' => 5, + 'quadrennial' => 6, + 'quinquennial' => 7 + ]; + + $is_upgrade = ( + $new_storage_tb > $current_storage_tb || + $new_users > $current_users || + (($frequency_order[$new_frequency] ?? 1) > ($frequency_order[$current_frequency] ?? 1)) + ); + + return $is_upgrade; +} + +/** + * Info detallada de suscripción basada en próxima fecha de pago (sin enddate). + */ +function nextcloud_banda_get_detailed_subscription_info($user_id) { + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + + $current_banda_level = null; + if (!empty($user_levels)) { + foreach ($user_levels as $level) { + if (in_array((int)$level->id, $allowed_levels, true)) { + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { + $current_banda_level = $level; + break; + } + } + } + } + + if (!$current_banda_level) { + return false; + } + + $current_config = nextcloud_banda_get_user_real_config_improved($user_id, $current_banda_level); + $current_amount = $current_config['final_amount'] ?: (float)$current_banda_level->initial_payment; + + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if (!$cycle_info) { + return false; + } + + return [ + 'current_amount' => $current_amount, + 'days_remaining' => isset($cycle_info['days_remaining']) ? $cycle_info['days_remaining'] : 0, + 'total_days' => isset($cycle_info['total_days']) ? $cycle_info['total_days'] : 1, + 'start_date' => date('Y-m-d H:i:s', isset($cycle_info['cycle_start']) ? $cycle_info['cycle_start'] : time()), + 'end_date' => date('Y-m-d H:i:s', isset($cycle_info['cycle_end']) ? $cycle_info['cycle_end'] : time()), + 'next_payment_date' => date('Y-m-d H:i:s', isset($cycle_info['next_payment_date']) ? $cycle_info['next_payment_date'] : time()), + 'current_config' => $current_config, + 'source' => 'next_payment_info' + ]; +} + +/** + * AJAX handler para cálculo de prorrateo en tiempo real - CORREGIDO + */ +function nextcloud_banda_ajax_calculate_proration() { + // Verificar autenticación + if (!is_user_logged_in()) { + wp_send_json_error([ + 'message' => __('Usuário não autenticado.', 'nextcloud-banda'), + ], 401); + return; + } + + // Verificar nonce + if (!check_ajax_referer('nextcloud_banda_proration', 'security', false)) { + wp_send_json_error([ + 'message' => __('Nonce inválido.', 'nextcloud-banda'), + ], 403); + return; + } + + // Obtener y sanitizar parámetros + $level_id = isset($_POST['level_id']) ? (int) $_POST['level_id'] : 0; + $storage_space_raw = isset($_POST['storage']) ? sanitize_text_field($_POST['storage']) : ''; + $num_users = isset($_POST['users']) ? (int) $_POST['users'] : 0; + $payment_frequency = isset($_POST['frequency']) ? strtolower(sanitize_text_field($_POST['frequency'])) : 'monthly'; + + // Validar parámetros requeridos + if ($level_id <= 0) { + wp_send_json_error([ + 'message' => __('ID do nível inválido.', 'nextcloud-banda'), + ], 400); + return; + } + + // Logging para debugging + nextcloud_banda_log_debug('AJAX proration request received', [ + 'level_id' => $level_id, + 'storage' => $storage_space_raw, + 'users' => $num_users, + 'frequency' => $payment_frequency, + 'user_id' => get_current_user_id() + ]); + + // Obtener configuración y validar frecuencia + $config = nextcloud_banda_get_config(); + $multipliers = $config['frequency_multipliers'] ?? ['monthly' => 1.0]; + + if (!isset($multipliers[$payment_frequency])) { + $payment_frequency = 'monthly'; + } + + // Normalizar parámetros + $storage_space = !empty($storage_space_raw) ? strtolower($storage_space_raw) : '1tb'; + if ($num_users < 1) { + $num_users = 2; + } + + // Obtener precio base + $level = pmpro_getLevel($level_id); + $base_price = $level && isset($level->initial_payment) + ? (float) $level->initial_payment + : NEXTCLOUD_BANDA_BASE_PRICE; + + // Calcular nuevo precio total + $new_total_price = (float) nextcloud_banda_calculate_pricing( + $storage_space, + $num_users, + $payment_frequency, + $base_price + ); + + // Obtener precio actual + $current_membership = pmpro_getMembershipLevelForUser(get_current_user_id()); + $current_base_price = !empty($current_membership) && isset($current_membership->initial_payment) + ? (float) $current_membership->initial_payment + : 0.0; + + nextcloud_banda_log_debug('--- AJAX PRORATION INPUT ---', [ + 'user_id' => get_current_user_id(), + 'level_id' => $level_id, + 'storage' => $storage_space, + 'users' => $num_users, + 'frequency' => $payment_frequency, + 'base_price' => $base_price, + 'new_total_price' => $new_total_price, + 'current_base_price' => $current_base_price, + ]); + + // Calcular prorrateo + $proration = nextcloud_banda_calculate_proration_core_aligned( + get_current_user_id(), + $level_id, + $storage_space, + $num_users, + $payment_frequency + ); + + // Verificar que el proration tenga datos válidos + if (!$proration || !is_array($proration)) { + nextcloud_banda_log_error('Proration calculation failed or returned invalid data', [ + 'user_id' => get_current_user_id(), + 'proration_result' => $proration + ]); + + wp_send_json_error([ + 'message' => __('Falha no cálculo de prorrateo.', 'nextcloud-banda'), + ], 500); + return; + } + + nextcloud_banda_log_debug('--- AJAX PRORATION RESULT ---', $proration); + + // Construir respuesta consistente + $response = [ + 'is_upgrade' => isset($proration['prorated_amount']) && (float)$proration['prorated_amount'] > 0, + 'new_total_price' => round($new_total_price, 2), + 'prorated_amount' => isset($proration['prorated_amount']) ? round((float)$proration['prorated_amount'], 2) : 0, + 'days_remaining' => isset($proration['days_remaining']) ? (int)$proration['days_remaining'] : 0, + 'total_days' => isset($proration['total_days']) ? (int)$proration['total_days'] : 1, + 'next_payment_date' => isset($proration['next_payment_date']) ? $proration['next_payment_date'] : '', + 'current_price' => isset($proration['current_price']) ? round((float)$proration['current_price'], 2) : round($current_base_price, 2), + 'new_price' => round($new_total_price, 2), + 'current_prorated_amount' => isset($proration['current_prorated_amount']) ? round((float)$proration['current_prorated_amount'], 2) : 0, + 'new_prorated_amount' => isset($proration['prorated_new_amount']) ? round((float)$proration['prorated_new_amount'], 2) : 0, + 'fraction_remaining' => isset($proration['fraction_remaining']) ? (float)$proration['fraction_remaining'] : 0, + 'current_frequency' => isset($proration['current_frequency']) ? $proration['current_frequency'] : $payment_frequency, + 'new_frequency' => isset($proration['new_frequency']) ? $proration['new_frequency'] : $payment_frequency, + 'current_cycle_label' => isset($proration['current_cycle_label']) ? $proration['current_cycle_label'] : '', + 'new_cycle_label' => isset($proration['new_cycle_label']) ? $proration['new_cycle_label'] : '', + 'message' => isset($proration['message']) ? $proration['message'] : 'Success', + 'success' => true + ]; + + nextcloud_banda_log_debug('--- AJAX PRORATION RESPONSE ---', $response); + + wp_send_json_success($response); +} + +// Registrar el endpoint AJAX +add_action('wp_ajax_nextcloud_banda_calculate_proration', 'nextcloud_banda_ajax_calculate_proration'); + +// ==== +// >>> PRORATION SYSTEM: end +// ==== + +// ==== +// GUARDADO DE CONFIGURACIÓN +// ==== + +/** + * Versión mejorada de guardado de configuración + */ +function nextcloud_banda_save_configuration($user_id, $morder) { + if (!$user_id || !$morder) { + nextcloud_banda_log_error('Invalid parameters for save configuration', [ + 'user_id' => $user_id, + 'morder' => !empty($morder) + ]); + return; + } + + // Validar campos requeridos + $required_fields = ['storage_space', 'num_users', 'payment_frequency']; + $config_data = []; + + foreach ($required_fields as $field) { + if (!isset($_REQUEST[$field])) { + nextcloud_banda_log_error('Missing required field in request', [ + 'user_id' => $user_id, + 'missing_field' => $field + ]); + return; + } + $config_data[$field] = sanitize_text_field(wp_unslash($_REQUEST[$field])); + } + + // Normalizar y validar configuración + $normalized_config = normalize_banda_config($config_data); + + // Preparar datos finales para guardar + $config = array_merge($normalized_config, [ + 'created_at' => current_time('mysql'), + 'updated_at' => current_time('mysql'), + 'level_id' => intval($morder->membership_id), + 'final_amount' => floatval($morder->InitialPayment), + 'order_id' => $morder->id ?? null, + 'version' => NEXTCLOUD_BANDA_PLUGIN_VERSION + ]); + + $config_json = wp_json_encode($config); + if ($config_json === false) { + nextcloud_banda_log_error('Failed to encode configuration JSON', [ + 'user_id' => $user_id, + 'config' => $config + ]); + return; + } + + // Limpiar datos anteriores antes de guardar nuevos + nextcloud_banda_delete_all_user_data($user_id); + + // Guardar nueva configuración + $result = update_user_meta($user_id, 'nextcloud_banda_config', $config_json); + + if ($result === false) { + nextcloud_banda_log_error('Failed to update user meta for configuration', [ + 'user_id' => $user_id + ]); + return; + } + + // Invalidar caché + nextcloud_banda_invalidate_user_cache($user_id); + + nextcloud_banda_log_info('Configuration saved successfully', [ + 'user_id' => $user_id, + 'config' => $config + ]); +} + +// ==== +// VISUALIZACIÓN DE CONFIGURACIÓN DEL MIEMBRO +// ==== + +/** + * Mapea cycle_number y cycle_period de PMPro a etiqueta legible en portugués + * CORREGIDO: Manejo de plurales y casos especiales + */ +function nextcloud_banda_map_cycle_label($cycle_number, $cycle_period) { + $period = strtolower((string)$cycle_period); + $num = (int)$cycle_number; + + // Normalizar períodos + if (strpos($period, 'month') !== false) { + $period = 'month'; + } elseif (strpos($period, 'year') !== false) { + $period = 'year'; + } elseif (strpos($period, 'week') !== false) { + $period = 'week'; + } elseif (strpos($period, 'day') !== false) { + $period = 'day'; + } + + if ($period === 'month') { + switch ($num) { + case 1: return 'Mensal'; + case 2: return 'Bimestral'; + case 3: return 'Trimestral'; + case 4: return 'Quadrimensal'; + case 5: return 'Quinquemestral'; + case 6: return 'Semestral'; + case 7: return 'Setembral'; + case 8: return 'Octomestral'; + case 9: return 'Nonomestral'; + case 10: return 'Decamensual'; + case 11: return 'Undecamensual'; + case 12: return 'Anual'; + case 18: return 'Anual e meio'; + case 24: return 'Bienal'; + case 30: return 'Biênio e meio'; + case 36: return 'Trienal'; + case 48: return 'Quadrienal'; + case 60: return 'Quinquenal'; + default: + if ($num > 1) { + return "{$num} meses"; + } else { + return "Mensal"; + } + } + } + + if ($period === 'year') { + if ($num === 1) { + return 'Anual'; + } else { + return "{$num} anos"; + } + } + + if ($period === 'week') { + if ($num === 1) { + return 'Semanal'; + } else { + return "{$num} semanas"; + } + } + + if ($period === 'day') { + if ($num === 1) { + return 'Diário'; + } else { + return "{$num} dias"; + } + } + + // Fallback genérico + return ucfirst($cycle_period) . " (" . $cycle_number . ")"; +} + +/** + * Deriva payment_frequency canónico desde cycle_number/cycle_period + * CORREGIDO: Manejo más preciso de conversiones + */ +function nextcloud_banda_derive_frequency_from_cycle($cycle_number, $cycle_period) { + $period = strtolower((string)$cycle_period); + $num = (int)$cycle_number; + + // Normalizar períodos + if (strpos($period, 'month') !== false) { + $period = 'month'; + } elseif (strpos($period, 'year') !== false) { + $period = 'year'; + } elseif (strpos($period, 'week') !== false) { + $period = 'week'; + } elseif (strpos($period, 'day') !== false) { + $period = 'day'; + } + + if ($period === 'month') { + switch ($num) { + case 1: return 'monthly'; + case 6: return 'semiannual'; + case 12: return 'annual'; + case 24: return 'biennial'; + case 36: return 'triennial'; + case 48: return 'quadrennial'; + case 60: return 'quinquennial'; + default: return 'monthly'; // fallback para otros meses + } + } + + if ($period === 'year') { + switch ($num) { + case 1: return 'annual'; + case 2: return 'biennial'; + case 3: return 'triennial'; + case 4: return 'quadrennial'; + case 5: return 'quinquennial'; + default: return 'annual'; // fallback para años + } + } + + if ($period === 'week') { + if ($num === 1) { + return 'weekly'; + } elseif ($num === 2) { + return 'biweekly'; + } else { + // Convertir semanas a meses aproximadamente + $months = round($num / 4.33); // 4.33 semanas por mes promedio + switch ($months) { + case 1: return 'monthly'; + case 3: return 'quarterly'; + case 6: return 'semiannual'; + case 12: return 'annual'; + default: return 'monthly'; + } + } + } + + if ($period === 'day') { + if ($num === 1) { + return 'daily'; + } else { + // Convertir días a meses aproximadamente + $months = round($num / 30); // 30 días por mes aproximado + switch ($months) { + case 1: return 'monthly'; + case 3: return 'quarterly'; + case 6: return 'semiannual'; + case 12: return 'annual'; + default: return 'monthly'; + } + } + } + + return 'monthly'; // fallback genérico +} + +function nextcloud_banda_show_member_config_improved() { + $user_id = get_current_user_id(); + if (!$user_id) { + nextcloud_banda_log_debug('No user logged in for member config display'); + return; + } + + try { + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + + if (empty($user_levels)) { + nextcloud_banda_log_debug("No memberships found for user {$user_id}"); + return; + } + + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + $banda_membership = null; + + foreach ($user_levels as $level) { + if (in_array((int)$level->id, $allowed_levels, true)) { + // Verificar que tenga ciclo activo usando next_payment_info + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + + // CORREGIDO: Validar que cycle_info tenga las claves necesarias + $cycle_end = isset($cycle_info['cycle_end']) ? (int)$cycle_info['cycle_end'] : 0; + + if ($cycle_end > time()) { + $banda_membership = $level; + break; + } + } + } + + if (!$banda_membership) { + nextcloud_banda_log_debug("No active Banda membership found for user {$user_id}"); + return; + } + + $membership = $banda_membership; + $real_config = nextcloud_banda_get_user_real_config_improved($user_id, $membership); + + $storage_options = nextcloud_banda_get_config('storage_options'); + $user_options = nextcloud_banda_get_config('user_options'); + + $frequency_labels = [ + 'monthly' => 'Mensal', + 'semiannual' => 'Semestral (-5%)', + 'annual' => 'Anual (-10%)', + 'biennial' => 'Bienal (-15%)', + 'triennial' => 'Trienal (-20%)', + 'quadrennial' => 'Quadrienal (-25%)', + 'quinquennial' => 'Quinquenal (-30%)' + ]; + + $used_space_tb = nextcloud_banda_get_used_space_tb($user_id); + + // Obtener próxima fecha de pago usando la nueva función + $tz = wp_timezone(); + $next_payment_date = ''; + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + + // CORREGIDO: Validar que cycle_info tenga las claves necesarias + $next_payment_ts = isset($cycle_info['next_payment_date']) ? (int)$cycle_info['next_payment_date'] : 0; + + if ($next_payment_ts > 0) { + $next_payment_date = wp_date('d/m/Y', $next_payment_ts, $tz); + } else { + $next_payment_date = __('Assinatura ativa até cancelamento', 'pmpro-banda'); + } + + $base_users_included = nextcloud_banda_get_config('base_users_included'); + + $display_storage = $real_config['storage_space'] ?: '1tb'; + $display_users = $real_config['num_users'] ?: $base_users_included; + + // CORREGIDO: Derivar frecuencia desde el ciclo real del level (fuente de verdad) + // y asegurar consistencia con la etiqueta mostrada + $cycle_number = (int)($membership->cycle_number ?? 1); + $cycle_period = (string)($membership->cycle_period ?? 'Month'); + + // Derivar la frecuencia canónica desde el ciclo + $derived_frequency = nextcloud_banda_derive_frequency_from_cycle($cycle_number, $cycle_period); + + // Generar la etiqueta legible desde el ciclo real (esto es lo que se debe mostrar) + $cycle_label = nextcloud_banda_map_cycle_label($cycle_number, $cycle_period); + + // Usar el ciclo real como fuente de verdad, no la frecuencia guardada + $display_frequency = $derived_frequency; + + $display_amount = $real_config['final_amount'] ?: (float)$membership->initial_payment; + + $additional_users = max(0, (int)$display_users - $base_users_included); + $is_estimated = ($real_config['source'] === 'none' || $real_config['source'] === 'membership_deduction'); + + ?> + + $real_config['source'], + 'is_estimated' => $is_estimated, + 'cycle_label' => $cycle_label, + 'derived_frequency' => $derived_frequency, + 'cycle_number' => $cycle_number, + 'cycle_period' => $cycle_period + ]); + + } catch (Exception $e) { + nextcloud_banda_log_error('Exception in nextcloud_banda_show_member_config_improved', [ + 'user_id' => $user_id, + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + + if (defined('WP_DEBUG') && WP_DEBUG && current_user_can('manage_options')) { + echo ''; + } + } +} + +// ==== +// MANEJO DE ELIMINACIÓN COMPLETA DE DATOS +// ==== + +/** + * Elimina todos los datos de configuración de Banda para un usuario + */ +function nextcloud_banda_delete_all_user_data($user_id) { + // Eliminar user meta específicos + delete_user_meta($user_id, 'nextcloud_banda_config'); + delete_user_meta($user_id, 'storage_space'); + delete_user_meta($user_id, 'num_users'); + delete_user_meta($user_id, 'payment_frequency'); + + // Eliminar campos de PMPro Register Helper si existen + if (function_exists('pmprorh_getProfileFields')) { + $fields = ['storage_space', 'num_users', 'payment_frequency']; + foreach ($fields as $field_name) { + delete_user_meta($user_id, $field_name); + } + } + + // Invalidar toda la caché del usuario + nextcloud_banda_invalidate_user_cache($user_id); + + nextcloud_banda_log_info("Todos los datos de Banda eliminados para user_id: {$user_id}"); +} + +/** + * Hook para eliminar datos cuando se elimina un usuario de WordPress + */ +add_action('delete_user', 'nextcloud_banda_cleanup_on_user_deletion'); +function nextcloud_banda_cleanup_on_user_deletion($user_id) { + nextcloud_banda_delete_all_user_data($user_id); + nextcloud_banda_log_info("Limpieza completada al eliminar usuario: {$user_id}"); +} + +/** + * Hook mejorado para eliminar configuración al cancelar membresía + */ +add_action('pmpro_after_cancel_membership_level', 'nextcloud_banda_clear_config_on_cancellation_improved', 10, 3); +function nextcloud_banda_clear_config_on_cancellation_improved($user_id, $membership_level_id, $cancelled_levels) { + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + + // Verificar si alguno de los niveles cancelados es de Banda + $has_banda_level = false; + if (is_array($cancelled_levels)) { + foreach ($cancelled_levels as $level) { + if (in_array((int)$level->membership_id, $allowed_levels, true)) { + $has_banda_level = true; + break; + } + } + } else { + // Compatibilidad con versiones anteriores + if (is_object($cancelled_levels) && isset($cancelled_levels->membership_id)) { + if (in_array((int)$cancelled_levels->membership_id, $allowed_levels, true)) { + $has_banda_level = true; + } + } + } + + if ($has_banda_level) { + nextcloud_banda_delete_all_user_data($user_id); + nextcloud_banda_log_info("Configuración eliminada tras cancelación de membresía Banda", [ + 'user_id' => $user_id, + 'cancelled_levels' => is_array($cancelled_levels) ? array_map(function($l) { return $l->membership_id; }, $cancelled_levels) : 'single_level' + ]); + } +} + +/** + * Hook para limpiar datos cuando se cambia completamente de nivel + */ +add_action('pmpro_after_change_membership_level', function($level_id, $user_id) { + // Si el nuevo nivel no es de Banda, limpiar datos anteriores + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + if (!in_array((int)$level_id, $allowed_levels, true)) { + nextcloud_banda_delete_all_user_data($user_id); + } else { + // Invalidar caché pero mantener datos + nextcloud_banda_invalidate_user_cache($user_id); + } +}, 20, 2); // Prioridad más baja para ejecutarse después de otros hooks + +/** + * Función mejorada para obtener configuración del usuario + * Forzará valores por defecto si no hay membresía activa + * Sincroniza payment_frequency con el ciclo real del level + */ +function nextcloud_banda_get_user_real_config_improved($user_id, $membership = null) { + // Verificar si el usuario tiene membresía Banda activa + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + + $has_active_banda_membership = false; + $active_level = null; + + if (!empty($user_levels)) { + foreach ($user_levels as $level) { + if (in_array((int)$level->id, $allowed_levels, true)) { + // Verificar usando next_payment_info + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { + $has_active_banda_membership = true; + $active_level = $level; + break; + } + } + } + } + + // Si no tiene membresía Banda activa, retornar valores por defecto + if (!$has_active_banda_membership) { + return [ + 'storage_space' => '1tb', + 'num_users' => 2, + 'payment_frequency' => 'monthly', + 'final_amount' => null, + 'source' => 'defaults_no_active_membership' + ]; + } + + // Si tiene membresía activa, obtener configuración real + $real_config = nextcloud_banda_get_user_real_config($user_id, $membership); + + // Sincronizar payment_frequency con el ciclo real del level (fuente de verdad) + if ($active_level) { + $cycle_number = (int)($active_level->cycle_number ?? 1); + $cycle_period = (string)($active_level->cycle_period ?? 'Month'); + $derived_freq = nextcloud_banda_derive_frequency_from_cycle($cycle_number, $cycle_period); + + // Sobrescribir payment_frequency con la derivada del ciclo real + $real_config['payment_frequency'] = $derived_freq; + $real_config['cycle_number'] = $cycle_number; + $real_config['cycle_period'] = $cycle_period; + + nextcloud_banda_log('CONFIG_SYNC', [ + 'user_id' => $user_id, + 'level_id' => $active_level->id, + 'cycle_number' => $cycle_number, + 'cycle_period' => $cycle_period, + 'derived_frequency' => $derived_freq, + 'previous_frequency' => $real_config['payment_frequency'] ?? 'none' + ]); + } + + return $real_config; +} + +// ==== +// FUNCIONES DE LIMPIEZA PARA CASOS ESPECIALES +// ==== + +/** + * Función para limpiar datos de usuarios que ya no tienen membresía activa + */ +function nextcloud_banda_cleanup_inactive_users() { + $users_with_config = get_users([ + 'meta_key' => 'nextcloud_banda_config', + 'fields' => ['ID'] + ]); + + $cleaned_count = 0; + $allowed_levels = nextcloud_banda_get_config('allowed_levels'); + + foreach ($users_with_config as $user) { + $user_id = $user->ID; + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + + $has_active_banda_membership = false; + if (!empty($user_levels)) { + foreach ($user_levels as $level) { + if (in_array((int)$level->id, $allowed_levels, true)) { + // Verificar usando next_payment_info + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { + $has_active_banda_membership = true; + break; + } + } + } + } + + // Si no tiene membresía Banda activa, limpiar sus datos + if (!$has_active_banda_membership) { + nextcloud_banda_delete_all_user_data($user_id); + $cleaned_count++; + } + } + + nextcloud_banda_log_info("Limpieza de usuarios inactivos completada", [ + 'usuarios_revisados' => count($users_with_config), + 'usuarios_limpiados' => $cleaned_count + ]); + + return $cleaned_count; +} + +// Agregar endpoint para limpieza manual (solo para administradores) +add_action('wp_ajax_nextcloud_banda_cleanup_inactive', 'nextcloud_banda_cleanup_inactive_endpoint'); +function nextcloud_banda_cleanup_inactive_endpoint() { + if (!current_user_can('manage_options')) { + wp_die('Acceso denegado'); + } + + $cleaned_count = nextcloud_banda_cleanup_inactive_users(); + + wp_send_json_success([ + 'message' => "Limpieza completada. {$cleaned_count} usuarios procesados.", + 'cleaned_count' => $cleaned_count + ]); +} + +// ==== +// INICIALIZACIÓN Y HOOKS +// ==== + +// Hook de inicialización único +add_action('init', 'nextcloud_banda_add_dynamic_fields', 20); + +// Hook para validar antes del checkout +add_filter('pmpro_registration_checks', 'nextcloud_banda_validate_before_checkout'); +function nextcloud_banda_validate_before_checkout($pmpro_continue_registration) { + if (!$pmpro_continue_registration) { + return $pmpro_continue_registration; + } + + $validation = nextcloud_banda_validate_checkout_config(); + + if (is_wp_error($validation)) { + pmpro_setMessage($validation->get_error_message(), 'pmpro_error'); + return false; + } + + return true; +} + +// Hook principal de modificación de precio +add_filter('pmpro_checkout_level', 'nextcloud_banda_modify_level_pricing', 10, 2); + +// Mantiene el startdate original si el usuario permanece en el mismo level +add_filter('pmpro_checkout_start_date', function($startdate, $user_id, $level) { + if (empty($level) || empty($level->id)) { + return $startdate; + } + if (!function_exists('pmpro_hasMembershipLevel')) { + return $startdate; + } + if (!pmpro_hasMembershipLevel($level->id, $user_id)) { + return $startdate; + } + global $wpdb; + $old = $wpdb->get_var($wpdb->prepare( + "SELECT startdate FROM $wpdb->pmpro_memberships_users WHERE user_id = %d AND membership_id = %d AND status = 'active' ORDER BY id DESC LIMIT 1", + $user_id, $level->id + )); + if (!empty($old)) { + return $old; + } + return $startdate; +}, 10, 3); + +// Hooks de guardado +add_action('pmpro_after_checkout', 'nextcloud_banda_save_configuration', 10, 2); + +// Hook para mostrar configuración en área de miembros +// Modificar la función de visualización para usar la versión mejorada +remove_action('pmpro_account_bullets_bottom', 'nextcloud_banda_show_member_config'); +add_action('pmpro_account_bullets_bottom', 'nextcloud_banda_show_member_config_improved'); + +// Invalidación de caché +add_action('pmpro_after_change_membership_level', function($level_id, $user_id) { + nextcloud_banda_invalidate_user_cache($user_id); +}, 10, 2); + +nextcloud_banda_log_info('PMPro Banda Dynamic Pricing loaded - SYNCHRONIZED VERSION', [ + 'version' => NEXTCLOUD_BANDA_PLUGIN_VERSION, + 'php_version' => PHP_VERSION, + 'base_price_constant' => NEXTCLOUD_BANDA_BASE_PRICE, + 'normalize_function_exists' => function_exists('normalize_banda_config'), + 'real_config_function_exists' => function_exists('nextcloud_banda_get_user_real_config') +]); diff --git a/storage/snippets/pmpro-dynamic-pricing.json b/storage/snippets/pmpro-dynamic-pricing.json new file mode 100644 index 0000000..8c7af6f --- /dev/null +++ b/storage/snippets/pmpro-dynamic-pricing.json @@ -0,0 +1,98 @@ +{ + "description": "Agregar campos din\u00e1micos usando PMPro Register Helper", + "last_updated": "2025-08-24 17:23:24", + "active": true, + "hooks": { + "admin_notices": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + }, + "plugins_loaded": { + "type": "action", + "priority": 25, + "accepted_args": 1, + "auto_detected": true + }, + "init": { + "type": "action", + "priority": 20, + "accepted_args": 1, + "auto_detected": true + }, + "wp_loaded": { + "type": "action", + "priority": 5, + "accepted_args": 1, + "auto_detected": true + }, + "wp_enqueue_scripts": { + "type": "action", + "priority": 30, + "accepted_args": 1, + "auto_detected": true + }, + "pmpro_checkout_level": { + "type": "filter", + "priority": 1, + "accepted_args": 1, + "auto_detected": true + }, + "pmpro_after_checkout_level": { + "type": "filter", + "priority": 1, + "accepted_args": 1, + "auto_detected": true + }, + "pmpro_checkout_level_cost_text": { + "type": "filter", + "priority": 10, + "accepted_args": 2, + "auto_detected": true + }, + "pmpro_checkout_before_processing": { + "type": "action", + "priority": 1, + "accepted_args": 1, + "auto_detected": true + }, + "pmpro_checkout_order": { + "type": "filter", + "priority": 1, + "accepted_args": 1, + "auto_detected": true + }, + "pmpro_checkout_before_payment": { + "type": "action", + "priority": 1, + "accepted_args": 1, + "auto_detected": true + }, + "pmpro_after_checkout": { + "type": "action", + "priority": 10, + "accepted_args": 2, + "auto_detected": true + }, + "pmpro_account_bullets_bottom": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + }, + "wp_footer": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + }, + "admin_bar_menu": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + } + }, + "load_priority": 10 +} \ No newline at end of file diff --git a/storage/snippets/pmpro-dynamic-pricing.php b/storage/snippets/pmpro-dynamic-pricing.php new file mode 100644 index 0000000..7be888f --- /dev/null +++ b/storage/snippets/pmpro-dynamic-pricing.php @@ -0,0 +1,995 @@ + [10, 11, 12, 13, 14], + 'price_per_tb' => 120.00, + 'office_user_price' => 25.00, + 'frequency_multipliers' => [ + 'monthly' => 1.0, + 'semiannual' => 5.7, + 'annual' => 10.8, + 'biennial' => 20.4, + 'triennial' => 28.8, + 'quadrennial' => 36.0, + 'quinquennial' => 42.0 + ], + 'storage_options' => [ + '1tb' => '1 Terabyte', '2tb' => '2 Terabytes', '3tb' => '3 Terabytes', + '4tb' => '4 Terabytes', '5tb' => '5 Terabytes', '6tb' => '6 Terabytes', + '7tb' => '7 Terabytes', '8tb' => '8 Terabytes', '9tb' => '9 Terabytes', + '10tb' => '10 Terabytes', '15tb' => '15 Terabytes', '20tb' => '20 Terabytes', + '30tb' => '30 Terabytes', '40tb' => '40 Terabytes', '50tb' => '50 Terabytes', + '60tb' => '60 Terabytes', '70tb' => '70 Terabytes', '80tb' => '80 Terabytes', + '90tb' => '90 Terabytes', '100tb' => '100 Terabytes', '200tb' => '200 Terabytes', + '300tb' => '300 Terabytes', '400tb' => '400 Terabytes', '500tb' => '500 Terabytes' + ], + 'office_options' => [ + '20users' => '±20 usuários (CODE - Grátis)', + '30users' => '30 usuários (Business)', + '50users' => '50 usuários (Business)', + '80users' => '80 usuários (Business)', + '100users' => '100 usuários (Enterprise, -15%)', + '150users' => '150 usuários (Enterprise, -15%)', + '200users' => '200 usuários (Enterprise, -15%)', + '300users' => '300 usuários (Enterprise, -15%)', + '400users' => '400 usuários (Enterprise, -15%)', + '500users' => '500 usuários (Enterprise, -15%)' + ] + ]; + } + + return $key ? ($config[$key] ?? null) : $config; +} + +// ==================================================================== +// SISTEMA DE LOGGING OPTIMIZADO +// ==================================================================== + +/** + * Logging centralizado con niveles + */ +function nextcloud_log($level, $message, $context = []) { + static $log_level = null; + + if ($log_level === null) { + if (defined('WP_DEBUG') && WP_DEBUG) { + $log_level = 4; // DEBUG + } elseif (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + $log_level = 3; // INFO + } else { + $log_level = 1; // ERROR only + } + } + + $levels = [1 => 'ERROR', 2 => 'WARNING', 3 => 'INFO', 4 => 'DEBUG']; + + if ($level > $log_level) return; + + $log_message = sprintf( + '[PMPro Dynamic %s] %s', + $levels[$level], + $message + ); + + if (!empty($context)) { + $log_message .= ' | Context: ' . wp_json_encode($context, JSON_UNESCAPED_UNICODE); + } + + error_log($log_message); +} + +// Funciones de logging simplificadas +function nextcloud_log_error($message, $context = []) { + nextcloud_log(1, $message, $context); +} + +function nextcloud_log_warning($message, $context = []) { + nextcloud_log(2, $message, $context); +} + +function nextcloud_log_info($message, $context = []) { + nextcloud_log(3, $message, $context); +} + +function nextcloud_log_debug($message, $context = []) { + nextcloud_log(4, $message, $context); +} + +// ==================================================================== +// SISTEMA DE CACHÉ OPTIMIZADO +// ==================================================================== + +/** + * Obtener datos del caché + */ +function nextcloud_cache_get($key, $default = false) { + $cached = wp_cache_get($key, NEXTCLOUD_CACHE_GROUP); + if ($cached !== false) { + nextcloud_log_debug("Cache hit for key: {$key}"); + return $cached; + } + + nextcloud_log_debug("Cache miss for key: {$key}"); + return $default; +} + +/** + * Guardar datos en caché + */ +function nextcloud_cache_set($key, $data, $expiry = NEXTCLOUD_CACHE_EXPIRY) { + $result = wp_cache_set($key, $data, NEXTCLOUD_CACHE_GROUP, $expiry); + nextcloud_log_debug("Cache set for key: {$key}", ['success' => $result]); + return $result; +} + +/** + * Eliminar datos del caché + */ +function nextcloud_cache_delete($key) { + $result = wp_cache_delete($key, NEXTCLOUD_CACHE_GROUP); + nextcloud_log_debug("Cache deleted for key: {$key}", ['success' => $result]); + return $result; +} + +/** + * Invalidar caché de usuario + */ +function nextcloud_invalidate_user_cache($user_id) { + $keys = [ + "nextcloud_config_{$user_id}", + "pmpro_membership_{$user_id}", + "nextcloud_used_space_{$user_id}", + "last_payment_date_{$user_id}" + ]; + + foreach ($keys as $key) { + nextcloud_cache_delete($key); + } + + nextcloud_log_info("User cache invalidated", ['user_id' => $user_id]); +} + +// ==================================================================== +// VERIFICACIÓN DE DEPENDENCIAS OPTIMIZADA +// ==================================================================== + +/** + * Verifica que los plugins requeridos estén activos + */ +function nextcloud_check_dependencies() { + static $dependencies_checked = false; + static $dependencies_ok = false; + + if ($dependencies_checked) { + return $dependencies_ok; + } + + $missing_plugins = []; + + // Verificaciones críticas + if (!function_exists('pmprorh_add_registration_field')) { + $missing_plugins[] = 'PMPro Register Helper'; + nextcloud_log_error('PMPro Register Helper functions not found'); + } + + if (!function_exists('pmpro_getOption')) { + $missing_plugins[] = 'Paid Memberships Pro'; + nextcloud_log_error('PMPro core functions not found'); + } + + if (!class_exists('PMProRH_Field')) { + $missing_plugins[] = 'PMProRH_Field class'; + nextcloud_log_error('PMProRH_Field class not available'); + } + + // Verificación opcional + if (!class_exists('MemberOrder')) { + nextcloud_log_warning('MemberOrder class not available - some features may be limited'); + } + + // Admin notice + if (!empty($missing_plugins) && is_admin() && current_user_can('manage_options')) { + add_action('admin_notices', function() use ($missing_plugins) { + $plugins_list = implode(', ', $missing_plugins); + printf( + '

PMPro Dynamic Pricing: Los siguientes plugins son requeridos: %s

', + esc_html($plugins_list) + ); + }); + } + + $dependencies_ok = empty($missing_plugins); + $dependencies_checked = true; + + nextcloud_log_info('Dependencies check completed', [ + 'success' => $dependencies_ok, + 'missing_count' => count($missing_plugins) + ]); + + return $dependencies_ok; +} + +// ==================================================================== +// DETECCIÓN DE NIVEL ACTUAL OPTIMIZADA +// ==================================================================== + +/** + * Detecta el Level ID actual con múltiples estrategias + */ +function nextcloud_get_current_level_id() { + static $cached_level_id = null; + + if ($cached_level_id !== null) { + return $cached_level_id; + } + + // Estrategias de detección en orden de prioridad + $detectors = [ + 'global_checkout_level' => function() { + global $pmpro_checkout_level; + return isset($pmpro_checkout_level->id) ? (int)$pmpro_checkout_level->id : 0; + }, + 'get_level' => function() { + return !empty($_GET['level']) ? (int)sanitize_text_field($_GET['level']) : 0; + }, + 'get_pmpro_level' => function() { + return !empty($_GET['pmpro_level']) ? (int)sanitize_text_field($_GET['pmpro_level']) : 0; + }, + 'post_level' => function() { + return !empty($_POST['level']) ? (int)sanitize_text_field($_POST['level']) : 0; + }, + 'post_pmpro_level' => function() { + return !empty($_POST['pmpro_level']) ? (int)sanitize_text_field($_POST['pmpro_level']) : 0; + }, + 'global_level' => function() { + global $pmpro_level; + return isset($pmpro_level->id) ? (int)$pmpro_level->id : 0; + }, + 'session_level' => function() { + return !empty($_SESSION['pmpro_level']) ? (int)$_SESSION['pmpro_level'] : 0; + } + ]; + + foreach ($detectors as $source => $detector) { + $level_id = $detector(); + if ($level_id > 0) { + nextcloud_log_debug("Level ID detected from {$source}: {$level_id}"); + $cached_level_id = $level_id; + return $level_id; + } + } + + // Fallback: extraer de URL + if (function_exists('pmpro_getOption')) { + $checkout_page_slug = pmpro_getOption('checkout_page_slug'); + if (!empty($checkout_page_slug) && is_page($checkout_page_slug)) { + $request_uri = $_SERVER['REQUEST_URI'] ?? ''; + if (preg_match('/(?:^|[?&])(level|pmpro_level)=(\d+)/', $request_uri, $matches)) { + $level_id = (int)$matches[2]; + nextcloud_log_debug("Level ID extracted from URL: {$level_id}"); + $cached_level_id = $level_id; + return $level_id; + } + } + } + + $cached_level_id = 0; + nextcloud_log_warning('Could not detect Level ID, using default 0'); + return 0; +} + +// ==================================================================== +// CAMPOS DINÁMICOS OPTIMIZADOS +// ==================================================================== + +/** + * Añade campos dinámicos con validación robusta + */ +function nextcloud_add_dynamic_fields() { + static $fields_added = false; + static $initialization_attempted = false; + + if ($initialization_attempted && !$fields_added) { + return false; + } + + $initialization_attempted = true; + + if ($fields_added) { + nextcloud_log_debug('Dynamic fields already added, skipping'); + return true; + } + + nextcloud_log_info('Attempting to add dynamic fields'); + + if (!nextcloud_check_dependencies()) { + nextcloud_log_error('Dependencies missing, cannot add fields'); + return false; + } + + $current_level_id = nextcloud_get_current_level_id(); + $allowed_levels = nextcloud_get_config('allowed_levels'); + + if (!in_array($current_level_id, $allowed_levels, true)) { + nextcloud_log_info("Level {$current_level_id} not in allowed levels, skipping fields"); + return false; + } + + try { + $config = nextcloud_get_config(); + $fields = []; + + // Campo de almacenamiento + $fields[] = new PMProRH_Field( + 'storage_space', + 'select', + [ + 'label' => 'Espaço de armazenamento', + 'options' => $config['storage_options'], + 'profile' => true, + 'required' => false, + 'memberslistcsv' => true, + 'addmember' => true, + 'location' => 'after_level' + ] + ); + + // Campo de suite ofimática + $fields[] = new PMProRH_Field( + 'office_suite', + 'select', + [ + 'label' => 'Nextcloud Office ', + 'options' => $config['office_options'], + 'profile' => true, + 'required' => false, + 'memberslistcsv' => true, + 'addmember' => true, + 'location' => 'after_level', + 'divclass' => 'pmpro_checkout-field-office-suite bordered-field' + ] + ); + + // Campo de frecuencia + $frequency_options = [ + 'monthly' => 'Mensal', + 'semiannual' => 'Semestral (-5%)', + 'annual' => 'Anual (-10%)', + 'biennial' => 'Bienal (-15%)', + 'triennial' => 'Trienal (-20%)', + 'quadrennial' => 'Quadrienal (-25%)', + 'quinquennial' => 'Quinquenal (-30%)' + ]; + + $fields[] = new PMProRH_Field( + 'payment_frequency', + 'select', + [ + 'label' => 'Frequência de pagamento', + 'options' => $frequency_options, + 'profile' => true, + 'required' => false, + 'memberslistcsv' => true, + 'addmember' => true, + 'location' => 'after_level' + ] + ); + + // Campo de precio total + $fields[] = new PMProRH_Field( + 'total_price_display', + 'text', + [ + 'label' => 'Preço total', + 'profile' => false, + 'required' => false, + 'memberslistcsv' => false, + 'addmember' => false, + 'readonly' => true, + 'location' => 'after_level', + 'showrequired' => false, + 'divclass' => 'pmpro_checkout-field-price-display', + 'default' => 'R$ 0,00' + ] + ); + + // Añadir campos + $fields_added_count = 0; + foreach($fields as $field) { + pmprorh_add_registration_field('Configuração do plano', $field); + $fields_added_count++; + } + + $fields_added = true; + + nextcloud_log_info("Dynamic fields added successfully", [ + 'level_id' => $current_level_id, + 'fields_count' => $fields_added_count + ]); + + return true; + + } catch (Exception $e) { + nextcloud_log_error('Exception adding dynamic fields', [ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + + if (defined('WP_DEBUG') && WP_DEBUG) { + wp_die('Error en el sistema de membresías: ' . esc_html($e->getMessage())); + } + + return false; + } +} + +// ==================================================================== +// CÁLCULOS DE PRECIO OPTIMIZADOS +// ==================================================================== + +/** + * Calcula el precio total con caché y validaciones + */ +function nextcloud_calculate_pricing($storage_space, $office_suite, $payment_frequency, $base_price) { + // Validar parámetros + if (empty($storage_space) || empty($office_suite) || empty($payment_frequency)) { + nextcloud_log_warning('Missing parameters for price calculation'); + return $base_price; + } + + // Verificar caché + $cache_key = "pricing_{$storage_space}_{$office_suite}_{$payment_frequency}_{$base_price}"; + $cached_price = nextcloud_cache_get($cache_key); + if ($cached_price !== false) { + return $cached_price; + } + + $config = nextcloud_get_config(); + $price_per_tb = $config['price_per_tb']; + $office_user_price = $config['office_user_price']; + + // Calcular precio de almacenamiento + $storage_tb = (int)str_replace('tb', '', $storage_space); + $storage_price = $base_price + ($price_per_tb * max(0, $storage_tb - 1)); + + // Calcular precio de suite ofimática + $office_users = (int)str_replace('users', '', $office_suite); + $office_user_price = ($office_users < 100) ? $office_user_price : ($office_user_price - 3.75); + $office_price = ($office_users <= 20) ? 0 : ($office_user_price * $office_users); + + // Aplicar multiplicador de frecuencia + $multipliers = $config['frequency_multipliers']; + $frequency_multiplier = $multipliers[$payment_frequency] ?? 1.0; + + // Calcular precio total + $total_price = ceil(($storage_price + $office_price) * $frequency_multiplier); + + // Validar resultado + if ($total_price < $base_price || $total_price > ($base_price * 100)) { + nextcloud_log_warning('Calculated price seems unreasonable', [ + 'total_price' => $total_price, + 'base_price' => $base_price + ]); + } + + // Guardar en caché + nextcloud_cache_set($cache_key, $total_price, 300); // 5 minutos + + nextcloud_log_debug('Price calculated', [ + 'storage_space' => $storage_space, + 'office_suite' => $office_suite, + 'payment_frequency' => $payment_frequency, + 'total_price' => $total_price + ]); + + return $total_price; +} + +/** + * Configura la periodicidad del nivel + */ +function nextcloud_configure_billing_period($level, $payment_frequency, $total_price) { + if (empty($level) || !is_object($level)) { + nextcloud_log_error('Invalid level object provided'); + return $level; + } + + $billing_cycles = [ + 'monthly' => ['number' => 1, 'period' => 'Month'], + 'semiannual' => ['number' => 6, 'period' => 'Month'], + 'annual' => ['number' => 12, 'period' => 'Month'], + 'biennial' => ['number' => 24, 'period' => 'Month'], + 'triennial' => ['number' => 36, 'period' => 'Month'], + 'quadrennial' => ['number' => 48, 'period' => 'Month'], + 'quinquennial' => ['number' => 60, 'period' => 'Month'] + ]; + + $cycle_config = $billing_cycles[$payment_frequency] ?? $billing_cycles['monthly']; + + $level->cycle_number = $cycle_config['number']; + $level->cycle_period = $cycle_config['period']; + $level->billing_amount = $total_price; + $level->initial_payment = $total_price; + $level->trial_amount = 0; + $level->trial_limit = 0; + $level->recurring = true; + + // Preservar configuración de expiración + if (!isset($level->expiration_number) || empty($level->expiration_number)) { + $level->expiration_number = 0; + $level->expiration_period = ''; + nextcloud_log_debug('Level configured as unlimited'); + } + + nextcloud_log_info('Billing period configured', [ + 'payment_frequency' => $payment_frequency, + 'cycle_number' => $level->cycle_number, + 'billing_amount' => $level->billing_amount + ]); + + return $level; +} + +// ==================================================================== +// FUNCIONES AUXILIARES OPTIMIZADAS +// ==================================================================== + +/** + * Obtiene el espacio usado en Nextcloud (placeholder) + */ +function get_nextcloud_used_space_tb($user_id) { + // TODO: Implementar conexión real a API de Nextcloud + $cache_key = "used_space_{$user_id}"; + $cached = nextcloud_cache_get($cache_key); + + if ($cached !== false) { + return $cached; + } + + // Placeholder - implementar API real + $used_space_mb = 1200; + $used_space_tb = round($used_space_mb / 1024 / 1024, 2); + + nextcloud_cache_set($cache_key, $used_space_tb, 300); + return $used_space_tb; +} + +/** + * Obtiene el espacio usado desde Nextcloud (placeholder) + */ +function get_nextcloud_used_storage($user_id) { + // TODO: Implementar API call real a Nextcloud + return 0; +} + +/** + * Calcula días restantes en el ciclo actual + */ +function get_remaining_days_in_cycle($user_id, $frequency) { + if (!class_exists('MemberOrder')) { + nextcloud_log_warning('MemberOrder class not available'); + return 0; + } + + $last_order = new MemberOrder(); + $last_order->getLastMemberOrder($user_id, 'success'); + + if (empty($last_order->timestamp)) { + return 0; + } + + $last_payment_date = is_numeric($last_order->timestamp) + ? $last_order->timestamp + : strtotime($last_order->timestamp); + + $cycle_days = get_cycle_days($frequency); + $days_used = floor((current_time('timestamp') - $last_payment_date) / DAY_IN_SECONDS); + + return max(0, $cycle_days - $days_used); +} + +/** + * Obtiene la duración en días del ciclo + */ +function get_cycle_days($frequency) { + $cycles = [ + 'monthly' => 30, + 'semiannual' => 180, + 'annual' => 365, + 'biennial' => 730, + 'triennial' => 1095, + 'quadrennial' => 1460, + 'quinquennial' => 1825 + ]; + + return $cycles[$frequency] ?? 30; +} + +// ==================================================================== +// SISTEMA DE PRORRATEO OPTIMIZADO +// ==================================================================== + +/** + * Valida si un cambio de plan es permitido + */ +function is_change_allowed($current_config, $new_config) { + $cur_storage = (int)str_replace('tb', '', $current_config['storage_space']); + $new_storage = (int)str_replace('tb', '', $new_config['storage_space']); + + // Verificar downgrade de almacenamiento + if ($new_storage < $cur_storage) { + $used = get_nextcloud_used_space_tb(get_current_user_id()); + if ($used > $new_storage) { + return [false, "Não é possível reduzir para {$new_storage} TB pois você usa {$used} TB"]; + } + } + + return [true, ""]; +} + +/** + * Valida y aplica prorrateo para cambios + */ +function nextcloud_validate_and_prorate_changes($level) { + $user_id = get_current_user_id(); + + // Cargar configuración actual + $cache_key = "nextcloud_config_{$user_id}"; + $current_config_json = nextcloud_cache_get($cache_key); + + if ($current_config_json === false) { + $current_config_json = get_user_meta($user_id, 'nextcloud_config', true); + nextcloud_cache_set($cache_key, $current_config_json); + } + + $current_config = $current_config_json ? json_decode($current_config_json, true) : null; + + // Si no hay configuración previa → nuevo registro + if (!$current_config) { + nextcloud_log_debug("New registration detected for user {$user_id}"); + return $level; + } + + // Obtener nuevas selecciones + $new_storage = sanitize_text_field($_POST['storage_space'] ?? $current_config['storage_space']); + $new_suite = sanitize_text_field($_POST['office_suite'] ?? ($current_config['office_suite'] ?? '20users')); + $new_frequency = sanitize_text_field($_POST['payment_frequency'] ?? $current_config['payment_frequency']); + + // Detectar cambios + $has_changes = ( + $new_storage !== $current_config['storage_space'] || + $new_suite !== ($current_config['office_suite'] ?? '20users') || + $new_frequency !== $current_config['payment_frequency'] + ); + + if (!$has_changes) { + nextcloud_log_debug("No changes detected for user {$user_id}"); + return $level; + } + + // Preparar nueva configuración + $new_config = [ + 'storage_space' => $new_storage, + 'office_suite' => $new_suite, + 'payment_frequency' => $new_frequency, + 'level_id' => $level->id + ]; + + // Validar cambio + list($allowed, $error_message) = is_change_allowed($current_config, $new_config); + + if (!$allowed) { + nextcloud_log_error("Change rejected for user {$user_id}: {$error_message}"); + wp_die(__($error_message, 'pmpro')); + } + + // Aplicar prorrateo si es upgrade de almacenamiento + $current_tb = (int)str_replace('tb', '', $current_config['storage_space']); + $new_tb = (int)str_replace('tb', '', $new_storage); + + if ($new_tb > $current_tb) { + $level = apply_storage_upgrade_prorate($level, $user_id, $current_tb, $new_tb, $current_config['payment_frequency']); + nextcloud_log_info("Storage upgrade applied with prorate for user {$user_id}"); + } + + return $level; +} + +/** + * Aplica prorrateo para upgrade de almacenamiento + */ +function apply_storage_upgrade_prorate($level, $user_id, $current_tb, $new_tb, $current_frequency) { + $price_per_tb = nextcloud_get_config('price_per_tb'); + $full_price_diff = ($new_tb - $current_tb) * $price_per_tb; + + $days_remaining = get_remaining_days_in_cycle($user_id, $current_frequency); + + if ($days_remaining > 0) { + $total_days = get_cycle_days($current_frequency); + $prorated_amount = ($full_price_diff / $total_days) * $days_remaining; + + $level->initial_payment += round($prorated_amount, 2); + + nextcloud_log_info("Prorate applied", [ + 'user_id' => $user_id, + 'upgrade_tb' => $new_tb - $current_tb, + 'days_remaining' => $days_remaining, + 'prorated_amount' => $prorated_amount + ]); + } + + return $level; +} + +// ==================================================================== +// HOOKS Y FILTROS PRINCIPALES +// ==================================================================== + +/** + * Hook principal de modificación de precio + */ +function nextcloud_modify_level_pricing($level) { + // Prevenir procesamiento múltiple + if (!empty($level->_nextcloud_applied)) { + nextcloud_log_debug('Level pricing already applied'); + return $level; + } + + $allowed_levels = nextcloud_get_config('allowed_levels'); + if (!in_array((int)$level->id, $allowed_levels, true)) { + return $level; + } + + $required_fields = ['storage_space', 'office_suite', 'payment_frequency']; + foreach ($required_fields as $field) { + if (!isset($_POST[$field]) || empty($_POST[$field])) { + nextcloud_log_debug("Required field {$field} missing"); + return $level; + } + } + + try { + // Aplicar validaciones y prorrateo + $level = nextcloud_validate_and_prorate_changes($level); + + // Sanitizar entrada + $storage_space = sanitize_text_field($_POST['storage_space']); + $office_suite = sanitize_text_field($_POST['office_suite']); + $payment_frequency = sanitize_text_field($_POST['payment_frequency']); + + // Obtener precio base original + $original_level = pmpro_getLevel($level->id); + $base_price = $original_level ? (float)$original_level->initial_payment : (float)$level->initial_payment; + + // Calcular precio total + $total_price = nextcloud_calculate_pricing($storage_space, $office_suite, $payment_frequency, $base_price); + + // Aplicar configuración + $level->initial_payment = $total_price; + $level = nextcloud_configure_billing_period($level, $payment_frequency, $total_price); + $level->_nextcloud_applied = true; + + nextcloud_log_info('Level pricing modified successfully', [ + 'level_id' => $level->id, + 'final_price' => $total_price, + 'storage_space' => $storage_space, + 'office_suite' => $office_suite, + 'payment_frequency' => $payment_frequency + ]); + + } catch (Exception $e) { + nextcloud_log_error('Exception in nextcloud_modify_level_pricing', [ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + + return $level; +} + +/** + * Guardado optimizado de configuración + */ +function nextcloud_save_configuration_and_provision($user_id, $morder) { + if (!$user_id || !$morder) { + nextcloud_log_error('Invalid parameters for save_configuration'); + return; + } + + $required_fields = ['storage_space', 'office_suite', 'payment_frequency']; + $config_data = []; + + foreach ($required_fields as $field) { + if (!isset($_REQUEST[$field]) || empty($_REQUEST[$field])) { + nextcloud_log_warning("Missing {$field} in configuration save"); + return; + } + $config_data[$field] = sanitize_text_field($_REQUEST[$field]); + } + + $config = array_merge($config_data, [ + 'created_at' => current_time('mysql'), + 'updated_at' => current_time('mysql'), + 'level_id' => $morder->membership_id, + 'final_amount' => $morder->InitialPayment, + 'order_id' => $morder->id ?? null, + 'version' => NEXTCLOUD_PLUGIN_VERSION + ]); + + $config_json = wp_json_encode($config); + + if (json_last_error() !== JSON_ERROR_NONE) { + nextcloud_log_error('JSON encoding error', ['error' => json_last_error_msg()]); + return; + } + + $saved = update_user_meta($user_id, 'nextcloud_config', $config_json); + + // Invalidar caché + nextcloud_invalidate_user_cache($user_id); + + if (!$saved) { + nextcloud_log_error('Failed to save user configuration', ['user_id' => $user_id]); + } else { + nextcloud_log_info('Configuration saved successfully', [ + 'user_id' => $user_id, + 'config' => $config + ]); + } +} + +/** + * Localización de script JS con datos optimizados + */ +function nextcloud_localize_pricing_script() { + // Verificar páginas relevantes + $is_relevant_page = false; + + if (function_exists('pmpro_getOption')) { + $checkout_page = pmpro_getOption('checkout_page_slug'); + $billing_page = pmpro_getOption('billing_page_slug'); + $account_page = pmpro_getOption('account_page_slug'); + + $is_relevant_page = ( + (!empty($checkout_page) && is_page($checkout_page)) || + (!empty($billing_page) && is_page($billing_page)) || + (!empty($account_page) && is_page($account_page)) + ); + } + + if (!$is_relevant_page) { + return; + } + + // Obtener datos del nivel actual + $level_id = nextcloud_get_current_level_id(); + $base_price = 0; + + if ($level_id > 0) { + $level = pmpro_getLevel($level_id); + $base_price = $level ? (float)$level->initial_payment : 0; + } + + // Datos del usuario actual + $current_storage = '1tb'; + $current_suite = '20users'; + $used_space_tb = 0; + + if (is_user_logged_in()) { + $user_id = get_current_user_id(); + $config_json = get_user_meta($user_id, 'nextcloud_config', true); + + if ($config_json) { + $config = json_decode($config_json, true); + $current_storage = $config['storage_space'] ?? '1tb'; + $current_suite = $config['office_suite'] ?? '20users'; + } + + $used_space_tb = get_nextcloud_used_space_tb($user_id); + } + + // Localizar script + $script_handle = 'simply-snippet-pmpro-dynamic-pricing'; + + wp_localize_script( + $script_handle, + 'nextcloud_pricing', + [ + 'level_id' => $level_id, + 'base_price' => $base_price, + 'currency_symbol' => 'R$', + 'current_storage' => $current_storage, + 'used_space_tb' => $used_space_tb, + 'current_suite' => $current_suite, + 'debug' => defined('WP_DEBUG') && WP_DEBUG, + 'timestamp' => time(), + 'ajax_url' => admin_url('admin-ajax.php'), + 'version' => NEXTCLOUD_PLUGIN_VERSION + ] + ); + + nextcloud_log_info('Script localized successfully', [ + 'base_price' => $base_price, + 'level_id' => $level_id + ]); +} + +// ==================================================================== +// INICIALIZACIÓN Y HOOKS +// ==================================================================== + +// Hooks de inicialización múltiples para compatibilidad +add_action('plugins_loaded', 'nextcloud_add_dynamic_fields', 25); +add_action('init', 'nextcloud_add_dynamic_fields', 20); +add_action('wp_loaded', 'nextcloud_add_dynamic_fields', 5); + +// Hook principal de modificación de precio +add_filter('pmpro_checkout_level', 'nextcloud_modify_level_pricing', 1); + +// Hooks de guardado +add_action('pmpro_after_checkout', 'nextcloud_save_configuration_and_provision', 10, 2); + +// Localización de scripts +add_action('wp_enqueue_scripts', 'nextcloud_localize_pricing_script', 30); + +// Invalidación de caché en cambios de membresía +add_action('pmpro_after_change_membership_level', function($level_id, $user_id) { + nextcloud_invalidate_user_cache($user_id); + nextcloud_log_info('Cache invalidated on membership change', [ + 'user_id' => $user_id, + 'level_id' => $level_id + ]); +}, 10, 2); + +// Indicador de estado en admin bar +add_action('admin_bar_menu', function($wp_admin_bar) { + if (!current_user_can('manage_options')) return; + + $dependencies_ok = nextcloud_check_dependencies(); + $status = $dependencies_ok ? '✅' : '❌'; + + $wp_admin_bar->add_node([ + 'id' => 'nextcloud-dynamic-status', + 'title' => "PMPro Dynamic {$status}", + 'href' => admin_url('plugins.php'), + 'meta' => ['title' => "PMPro Dynamic Status"] + ]); +}, 100); + +nextcloud_log_info('PMPro Dynamic Pricing snippet loaded successfully', [ + 'version' => NEXTCLOUD_PLUGIN_VERSION, + 'php_version' => PHP_VERSION +]); diff --git a/storage/snippets/simple-accordion.json b/storage/snippets/simple-accordion.json new file mode 100644 index 0000000..05a6551 --- /dev/null +++ b/storage/snippets/simple-accordion.json @@ -0,0 +1,7 @@ +{ + "description": "Custom Simple Accordion", + "last_updated": "2025-08-24 20:25:32", + "active": true, + "hooks": [], + "load_priority": 10 +} \ No newline at end of file diff --git a/storage/snippets/simple-accordion.php b/storage/snippets/simple-accordion.php new file mode 100644 index 0000000..759d0e7 --- /dev/null +++ b/storage/snippets/simple-accordion.php @@ -0,0 +1,55 @@ + +
+ +
+ '', + 'icon' => 'dashicons-menu', + 'open' => 'false', + ), $atts, 'accordion_item'); + + $title = $atts['title']; + $icon = $atts['icon']; + $is_open = ($atts['open'] === 'true' || $atts['open'] === '1'); + + $header_class = $is_open ? 'accordion-header active' : 'accordion-header'; + $content_class = $is_open ? 'accordion-content active' : 'accordion-content'; + $aria_expanded = $is_open ? 'true' : 'false'; + + ob_start(); + ?> +
+
+
+ + +
+ + +
+
+
+ +
+
+
+ IsSMTP(); + $phpmailer->Host = 'smtp-relay.sendinblue.com'; + $phpmailer->Port = 587; + $phpmailer->SMTPSecure = 'tls'; + $phpmailer->SMTPAuth = true; + $phpmailer->Username = 'jdavidcamejo@gmail.com'; + $phpmailer->Password = 'Ij8DcFwLxmZM6ytX'; + $phpmailer->From = 'web@brasdrive.com.br'; + $phpmailer->FromName = 'Brasdrive'; +} +add_action( 'phpmailer_init', 'setup_phpmailer_init' ); diff --git a/storage/snippets/theme-scripts.json b/storage/snippets/theme-scripts.json new file mode 100644 index 0000000..ebcdc06 --- /dev/null +++ b/storage/snippets/theme-scripts.json @@ -0,0 +1,26 @@ +{ + "description": "Agregar scripts y estilos en WP", + "last_updated": "2025-09-29 04:38:23", + "active": true, + "hooks": { + "wp_head": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + }, + "wp_footer": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + }, + "wp_enqueue_scripts": { + "type": "action", + "priority": 10, + "accepted_args": 1, + "auto_detected": true + } + }, + "load_priority": 10 +} \ No newline at end of file diff --git a/storage/snippets/theme-scripts.php b/storage/snippets/theme-scripts.php new file mode 100644 index 0000000..8be2c81 --- /dev/null +++ b/storage/snippets/theme-scripts.php @@ -0,0 +1,382 @@ + '1tb', + 'num_users' => 2, + 'payment_frequency' => 'monthly' + ]; + } + + $storage_space = sanitize_text_field($config_data['storage_space'] ?? '1tb'); + $valid_storage = ['1tb','2tb','3tb','4tb','5tb','6tb','7tb','8tb','9tb','10tb','15tb','20tb']; + if (!in_array($storage_space, $valid_storage, true)) { + $storage_space = '1tb'; + } + + $num_users = max(2, min(20, intval($config_data['num_users'] ?? 2))); + + $payment_frequency = sanitize_text_field($config_data['payment_frequency'] ?? 'monthly'); + $valid_frequencies = ['monthly','semiannual','annual','biennial','triennial','quadrennial','quinquennial']; + if (!in_array($payment_frequency, $valid_frequencies, true)) { + $payment_frequency = 'monthly'; + } + + return [ + 'storage_space' => $storage_space, + 'num_users' => $num_users, + 'payment_frequency' => $payment_frequency + ]; + } +} + +// Detecta posibles handles de script +function banda_detect_script_handles() { + global $wp_scripts; + + $possible_handles = [ + 'simply-snippet-nextcloud-banda-dynamic-pricing', + 'simply-code-nextcloud-banda-dynamic-pricing', + 'nextcloud-banda-dynamic-pricing', + 'banda-dynamic-pricing', + 'simply-snippet-banda-pricing', + 'simply-code-banda-pricing', + 'simply-snippet-nextcloud-banda', + 'simply-code-nextcloud-banda', + 'pmpro-banda-pricing', + 'banda-pricing-script' + ]; + + $detected_handles = []; + + if (isset($wp_scripts->registered)) { + foreach ($wp_scripts->registered as $handle => $script) { + if (stripos($handle, 'banda') !== false || + stripos($handle, 'nextcloud') !== false || + stripos($handle, 'pricing') !== false || + (stripos($handle, 'simply') !== false && stripos($handle, 'snippet') !== false)) { + $detected_handles[] = $handle; + } + } + } + + $all_handles = array_unique(array_merge($possible_handles, $detected_handles)); + + banda_theme_log('Script handles detection completed', [ + 'possible_count' => count($possible_handles), + 'detected_count' => count($detected_handles), + 'total_handles' => count($all_handles), + 'detected_handles' => array_slice($detected_handles, 0, 5) + ]); + + return $all_handles; +} + +// Función principal de localización mejorada - CORREGIDA +function banda_localize_pricing_script_improved() { + if (!function_exists('pmpro_getOption')) { + banda_theme_log('PMPro functions not available, skipping localization'); + return; + } + + $checkout_page_id = pmpro_getOption('checkout_page_id'); + $account_page_id = pmpro_getOption('account_page_id'); + + $is_relevant_page = ( + is_page($checkout_page_id) || + is_page($account_page_id) || + !empty($_GET['level']) + ); + + if (!$is_relevant_page) { + banda_theme_log('Not on relevant page, skipping localization'); + return; + } + + $level_id = 0; + if (!empty($_GET['level'])) { + $level_id = (int)sanitize_text_field($_GET['level']); + } elseif (!empty($_GET['pmpro_level'])) { + $level_id = (int)sanitize_text_field($_GET['pmpro_level']); + } elseif (function_exists('nextcloud_banda_get_current_level_id')) { + $level_id = nextcloud_banda_get_current_level_id(); + } + + $allowed_levels = function_exists('nextcloud_banda_get_config') + ? nextcloud_banda_get_config('allowed_levels') + : [2]; + + if (!in_array($level_id, $allowed_levels, true)) { + banda_theme_log('Level not allowed for localization', [ + 'level_id' => $level_id, + 'allowed_levels' => $allowed_levels, + ]); + return; + } + + $base_price = NEXTCLOUD_BANDA_BASE_PRICE; + if ($level_id > 0) { + $level = pmpro_getLevel($level_id); + if ($level && !empty($level->initial_payment) && $level->initial_payment > 0) { + $base_price = (float) $level->initial_payment; + } + } + + $current_storage = '1tb'; + $current_users = 2; + $current_frequency = 'monthly'; + $has_previous_config = false; + $used_space_tb = 0; + $next_payment_date = null; + $has_active_membership = false; + $current_subscription = null; + $current_price_paid = 0; + $last_credit_value = 0; + + if (is_user_logged_in()) { + $user_id = get_current_user_id(); + + if ( + function_exists('nextcloud_banda_get_next_payment_info') && + function_exists('nextcloud_banda_get_user_real_config_improved') + ) { + $user_levels = pmpro_getMembershipLevelsForUser($user_id); + + if (!empty($user_levels)) { + foreach ($user_levels as $l) { + if (in_array((int) $l->id, $allowed_levels, true)) { + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if ($cycle_info && !empty($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { + $has_active_membership = true; + break; + } + } + } + } + + if ($has_active_membership) { + $level = pmpro_getMembershipLevelForUser($user_id); + + if ($level) { + $real_config = nextcloud_banda_get_user_real_config_improved($user_id, $level); + + if (!empty($real_config) && $real_config['source'] !== 'defaults_no_active_membership') { + $current_storage = sanitize_text_field($real_config['storage_space'] ?? '1tb'); + $current_users = max(2, min(20, intval($real_config['num_users'] ?? 2))); + $current_frequency = sanitize_text_field($real_config['payment_frequency'] ?? 'monthly'); + $has_previous_config = true; + + $current_price_paid = !empty($real_config['current_cycle_amount']) + ? (float) $real_config['current_cycle_amount'] + : ((float) $level->initial_payment); + $last_credit_value = !empty($real_config['last_proration_credit']) + ? (float) $real_config['last_proration_credit'] + : 0.0; + } + } + + if ($level) { + $cycle_number = (int) ($level->cycle_number ?? 1); + $cycle_period = (string) ($level->cycle_period ?? 'Month'); + + $derived_frequency = function_exists('nextcloud_banda_derive_frequency_from_cycle') + ? nextcloud_banda_derive_frequency_from_cycle($cycle_number, $cycle_period) + : 'monthly'; + + $cycle_label = function_exists('nextcloud_banda_map_cycle_label') + ? nextcloud_banda_map_cycle_label($cycle_number, $cycle_period) + : 'Mensal'; + + $current_subscription = [ + 'storage_space' => $current_storage, + 'num_users' => $current_users, + 'payment_frequency' => $derived_frequency, + 'cycle_label' => $cycle_label, + 'cycle_number' => $cycle_number, + 'cycle_period' => $cycle_period, + 'final_amount' => !empty($level->initial_payment) ? (float) $level->initial_payment : 0, + 'current_price_paid' => $current_price_paid, + 'last_credit_value' => $last_credit_value, + 'subscription_end_date' => (!empty($level->enddate) && $level->enddate !== '0000-00-00 00:00:00') ? $level->enddate : null, + 'subscription_start_date' => !empty($level->startdate) ? $level->startdate : null, + ]; + } + + if (function_exists('nextcloud_banda_get_used_space_tb')) { + $used_space_tb = nextcloud_banda_get_used_space_tb($user_id); + } + + $cycle_info = nextcloud_banda_get_next_payment_info($user_id); + if ($cycle_info && !empty($cycle_info['next_payment_date'])) { + $next_payment_date = date('c', (int) $cycle_info['next_payment_date']); + } + } + } + } + + $configs_available = function_exists('nextcloud_banda_get_config'); + + $price_per_tb = $configs_available ? nextcloud_banda_get_config('price_per_tb') : 70.00; + $price_per_user = $configs_available ? nextcloud_banda_get_config('price_per_additional_user') : 10.00; + $base_users_included = $configs_available ? nextcloud_banda_get_config('base_users_included') : 2; + $base_storage_tb = $configs_available ? nextcloud_banda_get_config('base_storage_included') : 1; + $frequency_multipliers = $configs_available + ? nextcloud_banda_get_config('frequency_multipliers') + : [ + 'monthly' => 1.0, + 'semiannual' => 5.7, + 'annual' => 10.8, + 'biennial' => 20.4, + 'triennial' => 28.8, + 'quadrennial' => 36.0, + 'quinquennial'=> 42.0, + ]; + + $localization = [ + 'level_id' => $level_id, + 'base_price' => $base_price, + 'currency_symbol' => 'R$', + 'price_per_tb' => (float) $price_per_tb, + 'price_per_user' => (float) $price_per_user, + 'base_users_included' => (int) $base_users_included, + 'base_storage_included' => (int) $base_storage_tb, + 'current_storage' => $current_storage, + 'current_users' => $current_users, + 'current_frequency' => $current_frequency, + 'has_previous_config' => (bool) $has_previous_config, + 'hasActiveMembership' => (bool) $has_active_membership, + 'current_subscription_data'=> $current_subscription, + 'used_space_tb' => (float) $used_space_tb, + 'next_payment_date' => $next_payment_date, + 'frequency_multipliers' => $frequency_multipliers, + 'frequency_days' => [ + 'monthly' => 30, + 'semiannual' => 182, + 'annual' => 365, + 'biennial' => 730, + 'triennial' => 1095, + 'quadrennial' => 1460, + 'quinquennial'=> 1825, + ], + 'debug' => defined('WP_DEBUG') && WP_DEBUG, + 'version' => defined('NEXTCLOUD_BANDA_PLUGIN_VERSION') ? NEXTCLOUD_BANDA_PLUGIN_VERSION : '2.8.0', + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('nextcloud_banda_proration'), + 'base_price_constant' => NEXTCLOUD_BANDA_BASE_PRICE, + ]; + + $json = wp_json_encode($localization, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json === false) { + banda_theme_log('Failed to json_encode localization data'); + return; + } + + $inline_script = "window.nextcloud_banda_pricing = {$json};"; + $handles = banda_detect_script_handles(); + $localized = false; + + foreach ($handles as $handle) { + if (wp_script_is($handle, 'registered')) { + wp_add_inline_script($handle, $inline_script, 'before'); + banda_theme_log("Inline localization injected before handle: {$handle}"); + $localized = true; + break; + } + } + + // Fallbacks si no se pudo inyectar 'before' en ningún handle + if (!$localized) { + // Fallback 1: wp_head temprano + add_action('wp_head', function() use ($inline_script) { + echo "\n"; + }, 1); + + // Fallback 2: wp_footer como backup + add_action('wp_footer', function() use ($inline_script) { + echo "\n"; + }, 5); + + banda_theme_log('Localization injected via inline head/footer fallback', []); + } else { + banda_theme_log('Localization injected via wp_add_inline_script(before)', ['handles_checked' => count($handles)]); + } + + banda_theme_log('Localization process completed', [ + 'method' => $localized ? 'inline_before_handle' : 'inline_head_footer_fallback', + 'handles_checked' => count($handles), + 'base_price' => $base_price, + 'base_price_constant' => NEXTCLOUD_BANDA_BASE_PRICE, + 'has_subscription_data' => !empty($current_subscription_data) + ]); +} + +// Hooks para ejecutar la localización - MEJORADOS +add_action('wp_enqueue_scripts', 'banda_enqueue_banda_assets', 20); +function banda_enqueue_banda_assets() { + wp_enqueue_style('dashicons'); + banda_localize_pricing_script_improved(); +} + +// Fallback adicional para asegurar ejecución +add_action('wp_head', function() { + if (!did_action('banda_localize_pricing_script_improved')) { + banda_localize_pricing_script_improved(); + } +}, 999); + +// Función de logging simplificada +function banda_theme_log($message, $context = []) { + if (function_exists('nextcloud_banda_log_info')) { + nextcloud_banda_log_info('[Theme Scripts] ' . $message, $context); + } elseif (defined('WP_DEBUG') && WP_DEBUG) { + $log_message = '[Banda Theme] ' . $message; + if (!empty($context)) { + $log_message .= ' | ' . wp_json_encode($context, JSON_UNESCAPED_UNICODE); + } + error_log($log_message); + } +} + +// Enqueue de scripts personalizados adicionales +function enqueue_custom_contact_form_scripts() { + wp_enqueue_script('custom-contact-form', get_template_directory_uri() . '/js/custom-contact-form.js', array('jquery'), '1.0', true); + wp_localize_script('custom-contact-form', 'customContactForm', array( + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('custom_contact_form_nonce') + )); +} +add_action('wp_enqueue_scripts', 'enqueue_custom_contact_form_scripts'); + +// Log de inicialización +banda_theme_log('Theme Scripts loaded successfully - SYNCHRONIZED VERSION', [ + 'version' => '2.8.0', + 'base_price_constant' => NEXTCLOUD_BANDA_BASE_PRICE, + 'functions_available' => [ + 'normalize_banda_config' => function_exists('normalize_banda_config'), + 'pmpro_functions' => function_exists('pmpro_getOption'), + 'nextcloud_banda_functions' => function_exists('nextcloud_banda_get_config') + ] +]); diff --git a/templates/class.php b/templates/class.php new file mode 100644 index 0000000..a457b7d --- /dev/null +++ b/templates/class.php @@ -0,0 +1,11 @@ +