fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module - Normalize hooks to after_client_* and instantiate PerfexHooks safely - Fix OAuthController view path and API client class name - Add missing admin views for webhook config/logs; adjust view loading - Harden client portal routes and admin routes mapping - Make Dashboard/Logs/Queue tolerant to optional model methods - Align log details query with existing schema; avoid broken joins This makes the module operational in Perfex (admin + client), reduces 404s, and avoids fatal errors due to inconsistent tables/methods.
This commit is contained in:
857
modules/desk_moloni/assets/js/admin.js
Normal file
857
modules/desk_moloni/assets/js/admin.js
Normal file
@@ -0,0 +1,857 @@
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user