/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ /** * Desk-Moloni Admin JavaScript v3.0 * * Modern ES6+ JavaScript for admin interface * Features: Real-time updates, AJAX, Animations, Responsive behavior * * @package DeskMoloni\Assets * @version 3.0 */ class DeskMoloniAdmin { constructor() { this.apiUrl = window.location.origin + '/admin/desk_moloni/api/'; this.refreshInterval = 30000; // 30 seconds this.refreshIntervalId = null; this.isOnline = navigator.onLine; this.init(); } /** * Initialize the admin interface */ init() { this.bindEvents(); this.initTooltips(); this.setupAutoRefresh(); this.initProgressBars(); this.setupOfflineDetection(); // Load initial data if (this.isDashboard()) { this.loadDashboardData(); } console.log('🚀 Desk-Moloni Admin v3.0 initialized'); } /** * Bind event listeners */ bindEvents() { // Dashboard refresh button const refreshBtn = document.getElementById('refresh-dashboard'); if (refreshBtn) { refreshBtn.addEventListener('click', () => this.loadDashboardData(true)); } // Sync buttons document.querySelectorAll('[data-sync-action]').forEach(btn => { btn.addEventListener('click', (e) => this.handleSyncAction(e)); }); // Filter dropdowns document.querySelectorAll('[data-filter]').forEach(filter => { filter.addEventListener('click', (e) => this.handleFilter(e)); }); // Form submissions document.querySelectorAll('.desk-moloni-form').forEach(form => { form.addEventListener('submit', (e) => this.handleFormSubmit(e)); }); // Real-time search const searchInput = document.querySelector('[data-search]'); if (searchInput) { let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => this.performSearch(e.target.value), 300); }); } } /** * Initialize tooltips for status indicators */ initTooltips() { document.querySelectorAll('[data-tooltip]').forEach(element => { element.addEventListener('mouseenter', (e) => this.showTooltip(e)); element.addEventListener('mouseleave', (e) => this.hideTooltip(e)); }); } /** * Setup auto-refresh for dashboard */ setupAutoRefresh() { if (!this.isDashboard()) return; this.refreshIntervalId = setInterval(() => { if (this.isOnline && !document.hidden) { this.loadDashboardData(); } }, this.refreshInterval); // Pause refresh when page is hidden document.addEventListener('visibilitychange', () => { if (document.hidden) { clearInterval(this.refreshIntervalId); } else if (this.isDashboard()) { this.loadDashboardData(); this.setupAutoRefresh(); } }); } /** * Initialize animated progress bars */ initProgressBars() { document.querySelectorAll('.desk-moloni-progress__bar').forEach(bar => { const width = bar.dataset.width || '0'; // Animate to target width setTimeout(() => { bar.style.width = width + '%'; }, 100); }); } /** * Setup offline detection */ setupOfflineDetection() { window.addEventListener('online', () => { this.isOnline = true; this.showNotification('🌐 Connection restored', 'success'); if (this.isDashboard()) { this.loadDashboardData(); } }); window.addEventListener('offline', () => { this.isOnline = false; this.showNotification('📡 You are offline', 'warning'); }); } /** * Load dashboard data via AJAX */ async loadDashboardData(showLoader = false) { if (!this.isOnline) return; if (showLoader) { this.showLoader(); } try { const response = await this.apiCall('dashboard_data'); if (response.success) { this.updateDashboard(response.data); this.updateLastRefresh(); } else { this.showNotification('Failed to load dashboard data', 'error'); } } catch (error) { console.error('Dashboard data load error:', error); this.showNotification('Failed to connect to server', 'error'); } finally { if (showLoader) { this.hideLoader(); } } } /** * Update dashboard with new data */ updateDashboard(data) { // Update metrics this.updateElement('[data-metric="sync-count"]', data.sync_count || 0); this.updateElement('[data-metric="error-count"]', data.error_count || 0); this.updateElement('[data-metric="success-rate"]', (data.success_rate || 0) + '%'); this.updateElement('[data-metric="avg-time"]', (data.avg_execution_time || 0).toFixed(2) + 's'); // Update progress bars this.updateProgressBar('[data-progress="sync"]', data.sync_progress || 0); this.updateProgressBar('[data-progress="queue"]', data.queue_progress || 0); // Update recent activity if (data.recent_activity) { this.updateRecentActivity(data.recent_activity); } // Update status indicators this.updateSyncStatus(data.sync_status || 'idle'); } /** * Handle sync actions */ async handleSyncAction(event) { event.preventDefault(); const button = event.target.closest('[data-sync-action]'); const action = button.dataset.syncAction; const entityType = button.dataset.entityType || 'all'; button.disabled = true; button.innerHTML = ' Syncing...'; try { const response = await this.apiCall('sync', { action: action, entity_type: entityType }); if (response.success) { this.showNotification(`✅ ${action} sync completed successfully`, 'success'); this.loadDashboardData(); } else { this.showNotification(`❌ Sync failed: ${response.message}`, 'error'); } } catch (error) { this.showNotification('❌ Sync request failed', 'error'); } finally { button.disabled = false; button.innerHTML = button.dataset.originalText || 'Sync'; } } /** * Handle form submissions with AJAX */ async handleFormSubmit(event) { event.preventDefault(); const form = event.target; const formData = new FormData(form); const submitBtn = form.querySelector('[type="submit"]'); // Add CSRF token if (window.deskMoloniCSRF) { formData.append(window.deskMoloniCSRF.token_name, window.deskMoloniCSRF.token_value); } // Show loading state const originalText = submitBtn.textContent; submitBtn.disabled = true; submitBtn.innerHTML = ' Saving...'; try { const response = await fetch(form.action, { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { this.showNotification('✅ Settings saved successfully', 'success'); // Update CSRF token if provided if (result.csrf_token) { window.deskMoloniCSRF.updateToken(result.csrf_token); } } else { this.showNotification(`❌ ${result.message || 'Save failed'}`, 'error'); } } catch (error) { this.showNotification('❌ Failed to save settings', 'error'); } finally { submitBtn.disabled = false; submitBtn.textContent = originalText; } } /** * Utility: Make API calls */ async apiCall(endpoint, data = null) { const url = this.apiUrl + endpoint; const options = { method: data ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }; if (data) { // Add CSRF token to data if (window.deskMoloniCSRF) { data[window.deskMoloniCSRF.token_name] = window.deskMoloniCSRF.token_value; } options.body = JSON.stringify(data); } const response = await fetch(url, options); return await response.json(); } /** * Update element content with animation */ updateElement(selector, value) { const element = document.querySelector(selector); if (!element) return; const currentValue = element.textContent; if (currentValue !== value.toString()) { element.style.transform = 'scale(1.1)'; element.style.color = 'var(--dm-primary)'; setTimeout(() => { element.textContent = value; element.style.transform = 'scale(1)'; element.style.color = ''; }, 150); } } /** * Update progress bar */ updateProgressBar(selector, percentage) { const progressBar = document.querySelector(selector + ' .desk-moloni-progress__bar'); if (progressBar) { progressBar.style.width = percentage + '%'; progressBar.dataset.width = percentage; } } /** * Show notification */ showNotification(message, type = 'info') { // Create notification element if it doesn't exist let container = document.getElementById('desk-moloni-notifications'); if (!container) { container = document.createElement('div'); container.id = 'desk-moloni-notifications'; container.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 10000; display: flex; flex-direction: column; gap: 10px; `; document.body.appendChild(container); } const notification = document.createElement('div'); notification.className = `desk-moloni-notification desk-moloni-notification--${type}`; notification.style.cssText = ` padding: 12px 20px; border-radius: 6px; color: white; font-weight: 500; box-shadow: var(--dm-shadow-lg); transform: translateX(100%); transition: transform 0.3s ease; max-width: 300px; word-wrap: break-word; `; // Set background color based on type const colors = { success: 'var(--dm-success)', error: 'var(--dm-error)', warning: 'var(--dm-warning)', info: 'var(--dm-info)' }; notification.style.background = colors[type] || colors.info; notification.textContent = message; container.appendChild(notification); // Animate in setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 10); // Auto remove after 5 seconds setTimeout(() => { notification.style.transform = 'translateX(100%)'; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); }, 5000); } /** * Show/hide loader */ showLoader() { document.body.classList.add('desk-moloni-loading'); } hideLoader() { document.body.classList.remove('desk-moloni-loading'); } /** * Check if current page is dashboard */ isDashboard() { return window.location.href.includes('desk_moloni') && (window.location.href.includes('dashboard') || window.location.href.endsWith('desk_moloni')); } /** * Update last refresh time */ updateLastRefresh() { const element = document.querySelector('[data-last-refresh]'); if (element) { element.textContent = 'Last updated: ' + new Date().toLocaleTimeString(); } } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', function() { window.deskMoloniAdmin = new DeskMoloniAdmin(); }); function initDeskMoloni() { // Initialize components initSyncControls(); initConfigValidation(); initQueueMonitoring(); initLogViewer(); } /** * Initialize sync control buttons */ function initSyncControls() { const syncButtons = document.querySelectorAll('.desk-moloni-sync-btn'); syncButtons.forEach(function(button) { button.addEventListener('click', function(e) { e.preventDefault(); const action = this.dataset.action; const entityType = this.dataset.entityType; const entityId = this.dataset.entityId; performSync(action, entityType, entityId, this); }); }); } /** * Perform synchronization action */ function performSync(action, entityType, entityId, button) { // Show loading state const originalText = button.textContent; button.disabled = true; button.innerHTML = ' Syncing...'; // Prepare data const data = { action: action, entity_type: entityType, entity_id: entityId, csrf_token: getCSRFToken() }; // Make AJAX request fetch(admin_url + 'desk_moloni/admin/sync_action', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.success) { showNotification('Sync completed successfully', 'success'); refreshSyncStatus(); } else { showNotification('Sync failed: ' + (data.message || 'Unknown error'), 'error'); } }) .catch(error => { console.error('Sync error:', error); showNotification('Sync failed: Network error', 'error'); }) .finally(() => { // Restore button state button.disabled = false; button.textContent = originalText; }); } /** * Initialize configuration form validation */ function initConfigValidation() { const configForm = document.getElementById('desk-moloni-config-form'); if (configForm) { configForm.addEventListener('submit', function(e) { if (!validateConfigForm(this)) { e.preventDefault(); return false; } }); // Real-time validation for OAuth credentials const clientIdField = document.getElementById('oauth_client_id'); const clientSecretField = document.getElementById('oauth_client_secret'); if (clientIdField && clientSecretField) { clientIdField.addEventListener('blur', validateOAuthCredentials); clientSecretField.addEventListener('blur', validateOAuthCredentials); } } } /** * Validate configuration form */ function validateConfigForm(form) { let isValid = true; const errors = []; // Validate required fields const requiredFields = form.querySelectorAll('[required]'); requiredFields.forEach(function(field) { if (!field.value.trim()) { isValid = false; errors.push(field.getAttribute('data-label') + ' is required'); field.classList.add('error'); } else { field.classList.remove('error'); } }); // Validate OAuth Client ID format const clientId = form.querySelector('#oauth_client_id'); if (clientId && clientId.value && !isValidUUID(clientId.value)) { isValid = false; errors.push('OAuth Client ID must be a valid UUID'); clientId.classList.add('error'); } // Show errors if (!isValid) { showNotification('Please fix the following errors:\n' + errors.join('\n'), 'error'); } return isValid; } /** * Validate OAuth credentials */ function validateOAuthCredentials() { const clientId = document.getElementById('oauth_client_id').value; const clientSecret = document.getElementById('oauth_client_secret').value; if (clientId && clientSecret) { // Test OAuth credentials fetch(admin_url + 'desk_moloni/admin/test_oauth', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, csrf_token: getCSRFToken() }) }) .then(response => response.json()) .then(data => { const statusElement = document.getElementById('oauth-status'); if (statusElement) { if (data.valid) { statusElement.innerHTML = 'Valid'; } else { statusElement.innerHTML = 'Invalid'; } } }) .catch(error => { console.error('OAuth validation error:', error); }); } } /** * Initialize queue monitoring */ function initQueueMonitoring() { const queueTable = document.getElementById('desk-moloni-queue-table'); if (queueTable) { // Auto-refresh every 30 seconds setInterval(refreshQueueStatus, 30000); // Add action buttons functionality const actionButtons = queueTable.querySelectorAll('.queue-action-btn'); actionButtons.forEach(function(button) { button.addEventListener('click', function(e) { e.preventDefault(); const action = this.dataset.action; const queueId = this.dataset.queueId; performQueueAction(action, queueId); }); }); } } /** * Refresh queue status */ function refreshQueueStatus() { fetch(admin_url + 'desk_moloni/admin/get_queue_status', { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(data => { updateQueueTable(data.queue_items); updateQueueStats(data.stats); }) .catch(error => { console.error('Queue refresh error:', error); }); } /** * Update queue table */ function updateQueueTable(queueItems) { const tableBody = document.querySelector('#desk-moloni-queue-table tbody'); if (tableBody && queueItems) { tableBody.innerHTML = ''; queueItems.forEach(function(item) { const row = createQueueTableRow(item); tableBody.appendChild(row); }); } } /** * Create queue table row */ function createQueueTableRow(item) { const row = document.createElement('tr'); row.innerHTML = ` ${item.id} ${item.entity_type} ${item.entity_id} ${item.status} ${item.priority} ${item.attempts}/${item.max_attempts} ${item.created_at} ${item.status === 'failed' ? `` : ''} `; return row; } /** * Perform queue action */ function performQueueAction(action, queueId) { fetch(admin_url + 'desk_moloni/admin/queue_action', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ action: action, queue_id: queueId, csrf_token: getCSRFToken() }) }) .then(response => response.json()) .then(data => { if (data.success) { showNotification('Action completed successfully', 'success'); refreshQueueStatus(); } else { showNotification('Action failed: ' + (data.message || 'Unknown error'), 'error'); } }) .catch(error => { console.error('Queue action error:', error); showNotification('Action failed: Network error', 'error'); }); } /** * Initialize log viewer */ function initLogViewer() { const logViewer = document.getElementById('desk-moloni-log-viewer'); if (logViewer) { // Auto-refresh logs every 60 seconds setInterval(refreshLogs, 60000); // Add filter controls const filterForm = document.getElementById('log-filter-form'); if (filterForm) { filterForm.addEventListener('submit', function(e) { e.preventDefault(); refreshLogs(); }); } } } /** * Refresh logs */ function refreshLogs() { const filterForm = document.getElementById('log-filter-form'); const formData = new FormData(filterForm); const params = new URLSearchParams(formData); fetch(admin_url + 'desk_moloni/admin/get_logs?' + params.toString(), { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(data => { const logViewer = document.getElementById('desk-moloni-log-viewer'); if (logViewer && data.logs) { logViewer.innerHTML = data.logs.map(log => `
[${log.timestamp}] ${log.level.toUpperCase()}: ${log.message}
` ).join(''); // Scroll to bottom logViewer.scrollTop = logViewer.scrollHeight; } }) .catch(error => { console.error('Log refresh error:', error); }); } /** * Show notification */ function showNotification(message, type) { // Create notification element const notification = document.createElement('div'); notification.className = 'desk-moloni-notification ' + type; notification.textContent = message; // Insert at top of content area const contentArea = document.querySelector('.content-area') || document.body; contentArea.insertBefore(notification, contentArea.firstChild); // Auto-remove after 5 seconds setTimeout(function() { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 5000); } /** * Refresh sync status indicators */ function refreshSyncStatus() { fetch(admin_url + 'desk_moloni/admin/get_sync_status', { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(data => { updateSyncStatusElements(data); }) .catch(error => { console.error('Sync status refresh error:', error); }); } /** * Update sync status elements */ function updateSyncStatusElements(data) { // Update status indicators const statusElements = document.querySelectorAll('[data-sync-status]'); statusElements.forEach(function(element) { const entityType = element.dataset.syncStatus; if (data[entityType]) { element.className = 'desk-moloni-status ' + data[entityType].status; element.textContent = data[entityType].status; } }); } /** * Utility functions */ /** * Get CSRF token */ function getCSRFToken() { const tokenElement = document.querySelector('meta[name="csrf-token"]'); return tokenElement ? tokenElement.getAttribute('content') : ''; } /** * Validate UUID format */ function isValidUUID(uuid) { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(uuid); } /** * Update queue statistics */ function updateQueueStats(stats) { if (stats) { const statElements = { 'pending': document.getElementById('queue-stat-pending'), 'processing': document.getElementById('queue-stat-processing'), 'completed': document.getElementById('queue-stat-completed'), 'failed': document.getElementById('queue-stat-failed') }; Object.keys(statElements).forEach(function(key) { const element = statElements[key]; if (element && stats[key] !== undefined) { element.textContent = stats[key]; } }); } } })();