Files
desk-moloni/modules/desk_moloni/assets/js/admin.js
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 01:27:37 +01:00

862 lines
27 KiB
JavaScript

/**
* 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 = '<i class="fa fa-spinner fa-spin"></i> 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 = '<i class="fa fa-spinner fa-spin"></i> 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 = '<span class="desk-moloni-loading"></span> 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 = '<span class="desk-moloni-status success">Valid</span>';
} else {
statusElement.innerHTML = '<span class="desk-moloni-status error">Invalid</span>';
}
}
})
.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 = `
<td>${item.id}</td>
<td>${item.entity_type}</td>
<td>${item.entity_id}</td>
<td><span class="desk-moloni-status ${item.status}">${item.status}</span></td>
<td>${item.priority}</td>
<td>${item.attempts}/${item.max_attempts}</td>
<td>${item.created_at}</td>
<td>
${item.status === 'failed' ? `<button class="desk-moloni-btn desk-moloni-btn-small queue-action-btn" data-action="retry" data-queue-id="${item.id}">Retry</button>` : ''}
<button class="desk-moloni-btn desk-moloni-btn-danger desk-moloni-btn-small queue-action-btn" data-action="cancel" data-queue-id="${item.id}">Cancel</button>
</td>
`;
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 =>
`<div class="desk-moloni-log-line ${log.level}">[${log.timestamp}] ${log.level.toUpperCase()}: ${log.message}</div>`
).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];
}
});
}
}
})();