Files
desk-moloni/modules/desk_moloni/assets/js/queue_management.js
Emanuel Almeida c19f6fd9ee 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.
2025-09-11 17:38:45 +01:00

652 lines
24 KiB
JavaScript

/**
* Desk-Moloni Queue Management JavaScript
* Handles queue operations and real-time updates
*
* @package Desk-Moloni
* @version 3.0.0
* @author Descomplicar Business Solutions
*/
$(document).ready(function() {
'use strict';
// Queue Manager object
window.QueueManager = {
config: {
refreshInterval: 10000,
maxRetryAttempts: 3,
itemsPerPage: 50
},
state: {
currentPage: 1,
filters: {},
selectedTasks: [],
sortField: 'scheduled_at',
sortDirection: 'desc'
},
timers: {},
init: function() {
this.bindEvents();
this.initializeFilters();
this.loadQueue();
this.startAutoRefresh();
},
bindEvents: function() {
// Refresh queue
$('#refresh-queue').on('click', this.loadQueue.bind(this));
// Toggle processing
$('#toggle-processing').on('click', this.toggleProcessing.bind(this));
// Apply filters
$('#apply-filters').on('click', this.applyFilters.bind(this));
$('#clear-filters').on('click', this.clearFilters.bind(this));
// Filter changes
$('#queue-filters select, #queue-filters input').on('change', this.handleFilterChange.bind(this));
// Task selection
$(document).on('change', '#table-select-all', this.handleSelectAll.bind(this));
$(document).on('change', '.task-checkbox', this.handleTaskSelection.bind(this));
// Bulk actions
$(document).on('click', '[data-action]', this.handleBulkAction.bind(this));
// Individual task actions
$(document).on('click', '[data-task-action]', this.handleTaskAction.bind(this));
// Pagination
$(document).on('click', '.pagination a', this.handlePagination.bind(this));
// Sort handlers
$(document).on('click', '[data-sort]', this.handleSort.bind(this));
// Clear completed tasks
$('#clear-completed').on('click', this.clearCompleted.bind(this));
// Add task form
$('#add-task-form').on('submit', this.addTask.bind(this));
// Task details modal
$(document).on('click', '[data-task-details]', this.showTaskDetails.bind(this));
},
initializeFilters: function() {
// Set default date filters
var today = new Date();
var weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
$('#filter-date-from').val(weekAgo.toISOString().split('T')[0]);
$('#filter-date-to').val(today.toISOString().split('T')[0]);
},
loadQueue: function() {
var self = this;
var params = $.extend({}, this.state.filters, {
limit: this.config.itemsPerPage,
offset: (this.state.currentPage - 1) * this.config.itemsPerPage,
sort_field: this.state.sortField,
sort_direction: this.state.sortDirection
});
// Show loading state
$('#queue-table tbody').html('<tr><td colspan="9" class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading queue...</td></tr>');
$.ajax({
url: admin_url + 'modules/desk_moloni/queue/get_queue_status',
type: 'GET',
data: params,
dataType: 'json',
success: function(response) {
if (response.success) {
self.renderQueue(response.data);
self.updateSummary(response.data);
} else {
self.showError('Failed to load queue: ' + response.message);
}
},
error: function(xhr, status, error) {
self.showError('Failed to load queue data');
$('#queue-table tbody').html('<tr><td colspan="9" class="text-center text-danger">Failed to load queue data</td></tr>');
}
});
},
renderQueue: function(data) {
var tbody = $('#queue-table tbody');
tbody.empty();
if (!data.tasks || data.tasks.length === 0) {
tbody.html('<tr><td colspan="9" class="text-center">No tasks found</td></tr>');
return;
}
$.each(data.tasks, function(index, task) {
var row = QueueManager.createTaskRow(task);
tbody.append(row);
});
// Update pagination
this.updatePagination(data.pagination);
// Update selection state
this.updateSelectionControls();
},
createTaskRow: function(task) {
var statusClass = this.getStatusClass(task.status);
var priorityClass = this.getPriorityClass(task.priority);
var priorityLabel = this.getPriorityLabel(task.priority);
var actions = this.createTaskActions(task);
var row = '<tr data-task-id="' + task.id + '">' +
'<td><input type="checkbox" class="task-checkbox" value="' + task.id + '"></td>' +
'<td><strong>#' + task.id + '</strong></td>' +
'<td>' + this.formatTaskType(task.task_type) + '</td>' +
'<td>' + this.formatEntityInfo(task.entity_type, task.entity_id) + '</td>' +
'<td><span class="priority-badge priority-' + priorityClass + '">' + priorityLabel + '</span></td>' +
'<td><span class="label label-' + statusClass + '">' + task.status + '</span></td>' +
'<td>' + task.attempts + '/' + task.max_attempts + '</td>' +
'<td>' + this.formatDateTime(task.scheduled_at) + '</td>' +
'<td class="task-actions">' + actions + '</td>' +
'</tr>';
return row;
},
createTaskActions: function(task) {
var actions = [];
// Details button
actions.push('<button type="button" class="btn btn-xs btn-default" data-task-details="' + task.id + '" title="View Details"><i class="fa fa-info-circle"></i></button>');
// Retry button for failed tasks
if (task.status === 'failed' || task.status === 'retry') {
actions.push('<button type="button" class="btn btn-xs btn-warning" data-task-action="retry" data-task-id="' + task.id + '" title="Retry Task"><i class="fa fa-refresh"></i></button>');
}
// Cancel button for pending/processing tasks
if (task.status === 'pending' || task.status === 'processing') {
actions.push('<button type="button" class="btn btn-xs btn-danger" data-task-action="cancel" data-task-id="' + task.id + '" title="Cancel Task"><i class="fa fa-stop"></i></button>');
}
// Delete button for completed/failed tasks
if (task.status === 'completed' || task.status === 'failed') {
actions.push('<button type="button" class="btn btn-xs btn-danger" data-task-action="delete" data-task-id="' + task.id + '" title="Delete Task"><i class="fa fa-trash"></i></button>');
}
return actions.join(' ');
},
updateSummary: function(data) {
if (data.summary) {
$('#total-tasks').text(this.formatNumber(data.summary.total_tasks || 0));
$('#pending-tasks').text(this.formatNumber(data.summary.pending_tasks || 0));
$('#processing-tasks').text(this.formatNumber(data.summary.processing_tasks || 0));
$('#failed-tasks').text(this.formatNumber(data.summary.failed_tasks || 0));
}
},
updatePagination: function(pagination) {
var controls = $('#pagination-controls');
var info = $('#pagination-info');
controls.empty();
if (!pagination || pagination.total_pages <= 1) {
info.text('');
return;
}
// Pagination info
var start = ((pagination.current_page - 1) * pagination.per_page) + 1;
var end = Math.min(start + pagination.per_page - 1, pagination.total_items);
info.text('Showing ' + start + '-' + end + ' of ' + pagination.total_items + ' tasks');
// Previous button
if (pagination.current_page > 1) {
controls.append('<li><a href="#" data-page="' + (pagination.current_page - 1) + '">&laquo;</a></li>');
}
// Page numbers
var startPage = Math.max(1, pagination.current_page - 2);
var endPage = Math.min(pagination.total_pages, startPage + 4);
for (var i = startPage; i <= endPage; i++) {
var activeClass = i === pagination.current_page ? ' class="active"' : '';
controls.append('<li' + activeClass + '><a href="#" data-page="' + i + '">' + i + '</a></li>');
}
// Next button
if (pagination.current_page < pagination.total_pages) {
controls.append('<li><a href="#" data-page="' + (pagination.current_page + 1) + '">&raquo;</a></li>');
}
},
handleFilterChange: function(e) {
var $input = $(e.target);
var filterName = $input.attr('name');
var filterValue = $input.val();
this.state.filters[filterName] = filterValue;
},
applyFilters: function(e) {
e.preventDefault();
// Collect all filter values
$('#queue-filters input, #queue-filters select').each(function() {
var name = $(this).attr('name');
var value = $(this).val();
QueueManager.state.filters[name] = value;
});
this.state.currentPage = 1; // Reset to first page
this.loadQueue();
},
clearFilters: function(e) {
e.preventDefault();
// Clear form and state
$('#queue-filters')[0].reset();
this.state.filters = {};
this.state.currentPage = 1;
this.loadQueue();
},
handleSelectAll: function(e) {
var checked = $(e.target).is(':checked');
$('.task-checkbox').prop('checked', checked);
this.updateSelectedTasks();
},
handleTaskSelection: function(e) {
this.updateSelectedTasks();
// Update select all checkbox
var totalCheckboxes = $('.task-checkbox').length;
var checkedBoxes = $('.task-checkbox:checked').length;
$('#table-select-all').prop('indeterminate', checkedBoxes > 0 && checkedBoxes < totalCheckboxes);
$('#table-select-all').prop('checked', checkedBoxes === totalCheckboxes && totalCheckboxes > 0);
},
updateSelectedTasks: function() {
this.state.selectedTasks = $('.task-checkbox:checked').map(function() {
return parseInt($(this).val());
}).get();
// Show/hide bulk actions
if (this.state.selectedTasks.length > 0) {
$('#bulk-actions').show();
} else {
$('#bulk-actions').hide();
}
},
updateSelectionControls: function() {
// Clear selection when data refreshes
this.state.selectedTasks = [];
$('.task-checkbox').prop('checked', false);
$('#table-select-all').prop('checked', false).prop('indeterminate', false);
$('#bulk-actions').hide();
},
handleBulkAction: function(e) {
e.preventDefault();
if (this.state.selectedTasks.length === 0) {
this.showError('Please select tasks first');
return;
}
var action = $(e.target).closest('[data-action]').data('action');
var confirmMessage = this.getBulkActionConfirmMessage(action);
if (!confirm(confirmMessage)) {
return;
}
this.executeBulkAction(action, this.state.selectedTasks);
},
executeBulkAction: function(action, taskIds) {
var self = this;
$.ajax({
url: admin_url + 'modules/desk_moloni/queue/bulk_operation',
type: 'POST',
data: {
operation: action,
task_ids: taskIds
},
dataType: 'json',
success: function(response) {
if (response.success) {
self.showSuccess(response.message);
self.loadQueue();
} else {
self.showError(response.message);
}
},
error: function() {
self.showError('Bulk operation failed');
}
});
},
handleTaskAction: function(e) {
e.preventDefault();
var $btn = $(e.target).closest('[data-task-action]');
var action = $btn.data('task-action');
var taskId = $btn.data('task-id');
var confirmMessage = this.getTaskActionConfirmMessage(action);
if (confirmMessage && !confirm(confirmMessage)) {
return;
}
this.executeTaskAction(action, taskId);
},
executeTaskAction: function(action, taskId) {
var self = this;
var url = admin_url + 'modules/desk_moloni/queue/' + action + '_task/' + taskId;
$.ajax({
url: url,
type: 'POST',
dataType: 'json',
success: function(response) {
if (response.success) {
self.showSuccess(response.message);
self.loadQueue();
} else {
self.showError(response.message);
}
},
error: function() {
self.showError('Task action failed');
}
});
},
handlePagination: function(e) {
e.preventDefault();
var page = parseInt($(e.target).data('page'));
if (page && page !== this.state.currentPage) {
this.state.currentPage = page;
this.loadQueue();
}
},
handleSort: function(e) {
e.preventDefault();
var field = $(e.target).data('sort');
if (this.state.sortField === field) {
this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.state.sortField = field;
this.state.sortDirection = 'asc';
}
this.loadQueue();
},
toggleProcessing: function(e) {
e.preventDefault();
var self = this;
var $btn = $(e.target);
$.ajax({
url: admin_url + 'modules/desk_moloni/queue/toggle_processing',
type: 'POST',
dataType: 'json',
success: function(response) {
if (response.success) {
self.showSuccess(response.message);
self.updateToggleButton($btn, response.data.queue_processing_enabled);
} else {
self.showError(response.message);
}
},
error: function() {
self.showError('Failed to toggle processing');
}
});
},
updateToggleButton: function($btn, enabled) {
var icon = enabled ? 'fa-pause' : 'fa-play';
var text = enabled ? 'Pause Processing' : 'Resume Processing';
var btnClass = enabled ? 'btn-warning' : 'btn-success';
$btn.find('#toggle-processing-icon').removeClass().addClass('fa ' + icon);
$btn.find('#toggle-processing-text').text(text);
$btn.removeClass('btn-warning btn-success').addClass(btnClass);
},
clearCompleted: function(e) {
e.preventDefault();
if (!confirm('Are you sure you want to clear all completed tasks older than 7 days?')) {
return;
}
var self = this;
$.ajax({
url: admin_url + 'modules/desk_moloni/queue/clear_completed',
type: 'POST',
data: { days_old: 7 },
dataType: 'json',
success: function(response) {
if (response.success) {
self.showSuccess(response.message);
self.loadQueue();
} else {
self.showError(response.message);
}
},
error: function() {
self.showError('Failed to clear completed tasks');
}
});
},
addTask: function(e) {
e.preventDefault();
var $form = $(e.target);
var $submitBtn = $form.find('[type="submit"]');
// Validate JSON payload if provided
var payload = $('#payload').val();
if (payload) {
try {
JSON.parse(payload);
} catch (e) {
this.showError('Invalid JSON payload');
return;
}
}
this.showLoading($submitBtn);
var self = this;
$.ajax({
url: admin_url + 'modules/desk_moloni/queue/add_task',
type: 'POST',
data: $form.serialize(),
dataType: 'json',
success: function(response) {
if (response.success) {
self.showSuccess(response.message);
$('#add-task-modal').modal('hide');
$form[0].reset();
self.loadQueue();
} else {
self.showError(response.message);
}
},
error: function() {
self.showError('Failed to add task');
},
complete: function() {
self.hideLoading($submitBtn);
}
});
},
showTaskDetails: function(e) {
e.preventDefault();
var taskId = $(e.target).closest('[data-task-details]').data('task-details');
$('#task-details-modal').data('task-id', taskId).modal('show');
},
startAutoRefresh: function() {
var self = this;
this.timers.autoRefresh = setInterval(function() {
self.loadQueue();
}, this.config.refreshInterval);
},
stopAutoRefresh: function() {
if (this.timers.autoRefresh) {
clearInterval(this.timers.autoRefresh);
delete this.timers.autoRefresh;
}
},
// Helper methods
getStatusClass: function(status) {
switch (status) {
case 'completed': return 'success';
case 'processing': return 'info';
case 'failed': return 'danger';
case 'retry': return 'warning';
case 'pending': return 'default';
default: return 'default';
}
},
getPriorityClass: function(priority) {
switch (parseInt(priority)) {
case 1: return 'high';
case 5: return 'normal';
case 9: return 'low';
default: return 'normal';
}
},
getPriorityLabel: function(priority) {
switch (parseInt(priority)) {
case 1: return 'High';
case 5: return 'Normal';
case 9: return 'Low';
default: return 'Normal';
}
},
formatTaskType: function(taskType) {
return taskType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
},
formatEntityInfo: function(entityType, entityId) {
var icon = this.getEntityIcon(entityType);
return '<i class="fa ' + icon + '"></i> ' + this.formatTaskType(entityType) + ' #' + entityId;
},
getEntityIcon: function(entityType) {
switch (entityType) {
case 'client': return 'fa-user';
case 'product': return 'fa-cube';
case 'invoice': return 'fa-file-text';
case 'estimate': return 'fa-file-o';
case 'credit_note': return 'fa-file';
default: return 'fa-question';
}
},
getBulkActionConfirmMessage: function(action) {
switch (action) {
case 'retry':
return 'Are you sure you want to retry the selected tasks?';
case 'cancel':
return 'Are you sure you want to cancel the selected tasks?';
case 'delete':
return 'Are you sure you want to delete the selected tasks? This action cannot be undone.';
default:
return 'Are you sure you want to perform this action?';
}
},
getTaskActionConfirmMessage: function(action) {
switch (action) {
case 'cancel':
return 'Are you sure you want to cancel this task?';
case 'delete':
return 'Are you sure you want to delete this task? This action cannot be undone.';
default:
return null; // No confirmation needed
}
},
formatNumber: function(num) {
return new Intl.NumberFormat().format(num);
},
formatDateTime: function(dateString) {
if (!dateString) return 'N/A';
var date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
},
showLoading: function($element) {
var originalText = $element.data('original-text') || $element.html();
$element.data('original-text', originalText);
$element.prop('disabled', true)
.html('<i class="fa fa-spinner fa-spin"></i> Loading...');
},
hideLoading: function($element) {
var originalText = $element.data('original-text');
if (originalText) {
$element.html(originalText);
}
$element.prop('disabled', false);
},
showSuccess: function(message) {
if (typeof window.DeskMoloniAdmin !== 'undefined') {
window.DeskMoloniAdmin.showAlert('success', message);
} else {
alert(message);
}
},
showError: function(message) {
if (typeof window.DeskMoloniAdmin !== 'undefined') {
window.DeskMoloniAdmin.showAlert('danger', message);
} else {
alert(message);
}
}
};
// Initialize queue manager
window.QueueManager.init();
// Cleanup on page unload
$(window).on('beforeunload', function() {
window.QueueManager.stopAutoRefresh();
});
});