- 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.
652 lines
24 KiB
JavaScript
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) + '">«</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) + '">»</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();
|
|
});
|
|
}); |