Files
desk-moloni/modules/desk_moloni/controllers/Queue.php
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

544 lines
19 KiB
PHP

<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Desk-Moloni Queue Controller
* Handles queue management and bulk operations
*
* @package Desk-Moloni
* @version 3.0.0
* @author Descomplicar Business Solutions
*/
class Queue extends AdminController
{
public function __construct()
{
parent::__construct();
// Check if user is authenticated
if (!is_staff_logged_in()) {
redirect(admin_url('authentication'));
}
// Load models with correct names
$this->load->model('desk_moloni/desk_moloni_config_model', 'config_model');
$this->load->model('desk_moloni/desk_moloni_sync_queue_model', 'queue_model');
$this->load->model('desk_moloni/desk_moloni_mapping_model', 'mapping_model');
$this->load->model('desk_moloni/desk_moloni_sync_log_model', 'sync_log_model');
$this->load->helper('desk_moloni');
$this->load->library('form_validation');
}
/**
* Queue management interface
*/
public function index()
{
if (!has_permission('desk_moloni', '', 'view')) {
access_denied('desk_moloni');
}
$data = [
'title' => _l('desk_moloni_queue_management'),
'queue_summary' => $this->_get_queue_summary(),
'task_types' => $this->_get_task_types(),
'entity_types' => ['client', 'product', 'invoice', 'estimate', 'credit_note']
];
$this->load->view('admin/includes/header', $data);
$this->load->view('admin/modules/desk_moloni/queue_management', $data);
$this->load->view('admin/includes/footer');
}
/**
* Get queue status with pagination and filtering
*/
public function get_queue_status()
{
if (!has_permission('desk_moloni', '', 'view')) {
$this->output
->set_status_header(403)
->set_content_type('application/json')
->set_output(json_encode(['success' => false, 'message' => _l('access_denied')]));
return;
}
try {
$filters = [
'status' => $this->input->get('status'),
'entity_type' => $this->input->get('entity_type'),
'task_type' => $this->input->get('task_type'),
'priority' => $this->input->get('priority'),
'date_from' => $this->input->get('date_from'),
'date_to' => $this->input->get('date_to')
];
$pagination = [
'limit' => (int) $this->input->get('limit') ?: 50,
'offset' => (int) $this->input->get('offset') ?: 0
];
$queue_data = $this->queue_model->get_filtered_queue($filters, $pagination);
$this->output
->set_content_type('application/json')
->set_output(json_encode([
'success' => true,
'data' => [
'total_tasks' => $queue_data['total'],
'tasks' => $queue_data['tasks'],
'summary' => $this->queue_model->get_filtered_summary($filters),
'pagination' => [
'current_page' => floor($pagination['offset'] / $pagination['limit']) + 1,
'per_page' => $pagination['limit'],
'total_items' => $queue_data['total'],
'total_pages' => ceil($queue_data['total'] / $pagination['limit'])
]
]
]));
} catch (Exception $e) {
log_message('error', 'Desk-Moloni queue status error: ' . $e->getMessage());
$this->output
->set_status_header(500)
->set_content_type('application/json')
->set_output(json_encode([
'success' => false,
'message' => $e->getMessage()
]));
}
}
/**
* Add new sync task to queue
*/
public function add_task()
{
if (!has_permission('desk_moloni', '', 'create')) {
$this->output
->set_status_header(403)
->set_content_type('application/json')
->set_output(json_encode(['success' => false, 'message' => _l('access_denied')]));
return;
}
try {
$task_data = [
'task_type' => $this->input->post('task_type'),
'entity_type' => $this->input->post('entity_type'),
'entity_id' => (int) $this->input->post('entity_id'),
'priority' => (int) $this->input->post('priority') ?: 5,
'payload' => $this->input->post('payload') ? json_decode($this->input->post('payload'), true) : null
];
// Validate required fields
if (empty($task_data['task_type']) || empty($task_data['entity_type']) || empty($task_data['entity_id'])) {
throw new Exception(_l('desk_moloni_task_missing_required_fields'));
}
// Validate entity exists
if (!$this->_validate_entity_exists($task_data['entity_type'], $task_data['entity_id'])) {
throw new Exception(_l('desk_moloni_entity_not_found'));
}
$task_id = $this->queue_model->add_task($task_data);
$this->output
->set_content_type('application/json')
->set_output(json_encode([
'success' => true,
'message' => _l('desk_moloni_task_added_successfully'),
'data' => ['task_id' => $task_id]
]));
} catch (Exception $e) {
log_message('error', 'Desk-Moloni add task error: ' . $e->getMessage());
$this->output
->set_status_header(400)
->set_content_type('application/json')
->set_output(json_encode([
'success' => false,
'message' => $e->getMessage()
]));
}
}
/**
* Cancel sync task
*/
public function cancel_task($task_id)
{
if (!has_permission('desk_moloni', '', 'edit')) {
$this->output
->set_status_header(403)
->set_content_type('application/json')
->set_output(json_encode(['success' => false, 'message' => _l('access_denied')]));
return;
}
try {
$task_id = (int) $task_id;
if (!$task_id) {
throw new Exception(_l('desk_moloni_invalid_task_id'));
}
$result = $this->queue_model->cancel_task($task_id);
if (!$result) {
throw new Exception(_l('desk_moloni_task_cancel_failed'));
}
$this->output
->set_content_type('application/json')
->set_output(json_encode([
'success' => true,
'message' => _l('desk_moloni_task_cancelled_successfully')
]));
} catch (Exception $e) {
log_message('error', 'Desk-Moloni cancel task error: ' . $e->getMessage());
$this->output
->set_status_header(400)
->set_content_type('application/json')
->set_output(json_encode([
'success' => false,
'message' => $e->getMessage()
]));
}
}
/**
* Retry sync task
*/
public function retry_task($task_id)
{
if (!has_permission('desk_moloni', '', 'edit')) {
$this->output
->set_status_header(403)
->set_content_type('application/json')
->set_output(json_encode(['success' => false, 'message' => _l('access_denied')]));
return;
}
try {
$task_id = (int) $task_id;
if (!$task_id) {
throw new Exception(_l('desk_moloni_invalid_task_id'));
}
$result = $this->queue_model->retry_task($task_id);
if (!$result) {
throw new Exception(_l('desk_moloni_task_retry_failed'));
}
$this->output
->set_content_type('application/json')
->set_output(json_encode([
'success' => true,
'message' => _l('desk_moloni_task_retried_successfully')
]));
} catch (Exception $e) {
log_message('error', 'Desk-Moloni retry task error: ' . $e->getMessage());
$this->output
->set_status_header(400)
->set_content_type('application/json')
->set_output(json_encode([
'success' => false,
'message' => $e->getMessage()
]));
}
}
/**
* Bulk operations on tasks
*/
public function bulk_operation()
{
if (!has_permission('desk_moloni', '', 'edit')) {
$this->output
->set_status_header(403)
->set_content_type('application/json')
->set_output(json_encode(['success' => false, 'message' => _l('access_denied')]));
return;
}
try {
$operation = $this->input->post('operation');
$task_ids = $this->input->post('task_ids');
if (empty($operation) || empty($task_ids) || !is_array($task_ids)) {
throw new Exception(_l('desk_moloni_bulk_operation_invalid_params'));
}
$results = [];
$success_count = 0;
$error_count = 0;
foreach ($task_ids as $task_id) {
try {
$task_id = (int) $task_id;
$result = false;
switch ($operation) {
case 'retry':
$result = $this->queue_model->retry_task($task_id);
break;
case 'cancel':
$result = $this->queue_model->cancel_task($task_id);
break;
case 'delete':
$result = $this->queue_model->delete_task($task_id);
break;
case 'priority_high':
$result = $this->queue_model->set_task_priority($task_id, 1);
break;
case 'priority_normal':
$result = $this->queue_model->set_task_priority($task_id, 5);
break;
case 'priority_low':
$result = $this->queue_model->set_task_priority($task_id, 9);
break;
default:
throw new Exception(_l('desk_moloni_invalid_bulk_operation'));
}
if ($result) {
$success_count++;
$results[$task_id] = ['success' => true];
} else {
$error_count++;
$results[$task_id] = ['success' => false, 'error' => 'Operation failed'];
}
} catch (Exception $e) {
$error_count++;
$results[$task_id] = ['success' => false, 'error' => $e->getMessage()];
}
}
$this->output
->set_content_type('application/json')
->set_output(json_encode([
'success' => $success_count > 0,
'message' => sprintf(
_l('desk_moloni_bulk_operation_results'),
$success_count,
$error_count
),
'data' => [
'success_count' => $success_count,
'error_count' => $error_count,
'results' => $results
]
]));
} catch (Exception $e) {
log_message('error', 'Desk-Moloni bulk operation error: ' . $e->getMessage());
$this->output
->set_status_header(400)
->set_content_type('application/json')
->set_output(json_encode([
'success' => false,
'message' => $e->getMessage()
]));
}
}
/**
* Clear completed tasks from queue
*/
public function clear_completed()
{
if (!has_permission('desk_moloni', '', 'delete')) {
$this->output
->set_status_header(403)
->set_content_type('application/json')
->set_output(json_encode(['success' => false, 'message' => _l('access_denied')]));
return;
}
try {
$days_old = (int) $this->input->post('days_old') ?: 7;
$deleted_count = $this->queue_model->clear_completed_tasks($days_old);
$this->output
->set_content_type('application/json')
->set_output(json_encode([
'success' => true,
'message' => sprintf(
_l('desk_moloni_completed_tasks_cleared'),
$deleted_count
),
'data' => ['deleted_count' => $deleted_count]
]));
} catch (Exception $e) {
log_message('error', 'Desk-Moloni clear completed error: ' . $e->getMessage());
$this->output
->set_status_header(500)
->set_content_type('application/json')
->set_output(json_encode([
'success' => false,
'message' => $e->getMessage()
]));
}
}
/**
* Pause/Resume queue processing
*/
public function toggle_processing()
{
if (!has_permission('desk_moloni', '', 'edit')) {
$this->output
->set_status_header(403)
->set_content_type('application/json')
->set_output(json_encode(['success' => false, 'message' => _l('access_denied')]));
return;
}
try {
$current_status = $this->config_model->get_config('queue_processing_enabled') === '1';
$new_status = !$current_status;
$this->config_model->set_config('queue_processing_enabled', $new_status ? '1' : '0');
$message = $new_status
? _l('desk_moloni_queue_processing_resumed')
: _l('desk_moloni_queue_processing_paused');
$this->output
->set_content_type('application/json')
->set_output(json_encode([
'success' => true,
'message' => $message,
'data' => ['queue_processing_enabled' => $new_status]
]));
} catch (Exception $e) {
log_message('error', 'Desk-Moloni toggle processing error: ' . $e->getMessage());
$this->output
->set_status_header(500)
->set_content_type('application/json')
->set_output(json_encode([
'success' => false,
'message' => $e->getMessage()
]));
}
}
/**
* Get queue statistics
*/
public function get_statistics()
{
if (!has_permission('desk_moloni', '', 'view')) {
$this->output
->set_status_header(403)
->set_content_type('application/json')
->set_output(json_encode(['success' => false, 'message' => _l('access_denied')]));
return;
}
try {
$days = (int) $this->input->get('days') ?: 7;
$statistics = [
'queue_summary' => $this->queue_model->get_queue_summary(),
'processing_stats' => method_exists($this->queue_model, 'get_processing_statistics') ? $this->queue_model->get_processing_statistics($days) : [],
'performance_metrics' => method_exists($this->queue_model, 'get_performance_metrics') ? $this->queue_model->get_performance_metrics($days) : [],
'error_analysis' => method_exists($this->queue_model, 'get_error_analysis') ? $this->queue_model->get_error_analysis($days) : []
];
$this->output
->set_content_type('application/json')
->set_output(json_encode([
'success' => true,
'data' => $statistics
]));
} catch (Exception $e) {
log_message('error', 'Desk-Moloni queue statistics error: ' . $e->getMessage());
$this->output
->set_status_header(500)
->set_content_type('application/json')
->set_output(json_encode([
'success' => false,
'message' => $e->getMessage()
]));
}
}
/**
* Validate that entity exists in Perfex
*/
private function _validate_entity_exists($entity_type, $entity_id)
{
$this->load->model('clients_model');
$this->load->model('invoices_model');
$this->load->model('estimates_model');
$this->load->model('credit_notes_model');
$this->load->model('items_model');
switch ($entity_type) {
case 'client':
return $this->clients_model->get($entity_id) !== false;
case 'invoice':
return $this->invoices_model->get($entity_id) !== false;
case 'estimate':
return $this->estimates_model->get($entity_id) !== false;
case 'credit_note':
return $this->credit_notes_model->get($entity_id) !== false;
case 'product':
return $this->items_model->get($entity_id) !== false;
default:
return false;
}
}
/**
* Get queue summary statistics
*/
private function _get_queue_summary()
{
try {
return [
'total_tasks' => $this->queue_model->countTasks(),
'pending_tasks' => $this->queue_model->countTasks(['status' => 'pending']),
'processing_tasks' => $this->queue_model->countTasks(['status' => 'processing']),
'failed_tasks' => $this->queue_model->countTasks(['status' => 'failed']),
'completed_tasks' => $this->queue_model->countTasks(['status' => 'completed'])
];
} catch (Exception $e) {
log_message('error', 'Desk-Moloni queue summary error: ' . $e->getMessage());
return [];
}
}
/**
* Get available task types
*/
private function _get_task_types()
{
return [
'sync_client' => 'Client Sync',
'sync_product' => 'Product Sync',
'sync_invoice' => 'Invoice Sync',
'sync_estimate' => 'Estimate Sync',
'sync_credit_note' => 'Credit Note Sync'
];
}
}