/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ 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'); } /** * Mapping management interface */ public function index() { if (!has_permission('desk_moloni', '', 'view')) { access_denied('desk_moloni'); } $data = [ 'title' => _l('desk_moloni_mapping_management'), 'entity_types' => ['client', 'product', 'invoice', 'estimate', 'credit_note'], 'mapping_stats' => $this->mapping_model->get_mapping_statistics() ]; $this->load->view('admin/includes/header', $data); $this->load->view('admin/modules/desk_moloni/mapping_management', $data); $this->load->view('admin/includes/footer'); } /** * Get mappings with filtering and pagination */ public function get_mappings() { 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 = [ 'entity_type' => $this->input->get('entity_type'), 'sync_direction' => $this->input->get('sync_direction'), 'search' => $this->input->get('search'), 'last_sync_from' => $this->input->get('last_sync_from'), 'last_sync_to' => $this->input->get('last_sync_to') ]; $pagination = [ 'limit' => (int) $this->input->get('limit') ?: 50, 'offset' => (int) $this->input->get('offset') ?: 0 ]; $mapping_data = $this->mapping_model->get_filtered_mappings($filters, $pagination); // Enrich mappings with entity names foreach ($mapping_data['mappings'] as &$mapping) { $mapping['perfex_name'] = $this->_get_entity_name('perfex', $mapping['entity_type'], $mapping['perfex_id']); $mapping['moloni_name'] = $this->_get_entity_name('moloni', $mapping['entity_type'], $mapping['moloni_id']); } $this->output ->set_content_type('application/json') ->set_output(json_encode([ 'success' => true, 'data' => [ 'total' => $mapping_data['total'], 'mappings' => $mapping_data['mappings'], 'pagination' => [ 'current_page' => floor($pagination['offset'] / $pagination['limit']) + 1, 'per_page' => $pagination['limit'], 'total_items' => $mapping_data['total'], 'total_pages' => ceil($mapping_data['total'] / $pagination['limit']) ] ] ])); } catch (Exception $e) { log_message('error', 'Desk-Moloni get mappings error: ' . $e->getMessage()); $this->output ->set_status_header(500) ->set_content_type('application/json') ->set_output(json_encode([ 'success' => false, 'message' => $e->getMessage() ])); } } /** * Create manual mapping */ public function create_mapping() { 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 { $mapping_data = [ 'entity_type' => $this->input->post('entity_type'), 'perfex_id' => (int) $this->input->post('perfex_id'), 'moloni_id' => (int) $this->input->post('moloni_id'), 'sync_direction' => $this->input->post('sync_direction') ?: 'bidirectional' ]; // Validate required fields if (empty($mapping_data['entity_type']) || empty($mapping_data['perfex_id']) || empty($mapping_data['moloni_id'])) { throw new Exception(_l('desk_moloni_mapping_missing_required_fields')); } // Validate entity type if (!in_array($mapping_data['entity_type'], ['client', 'product', 'invoice', 'estimate', 'credit_note'])) { throw new Exception(_l('desk_moloni_invalid_entity_type')); } // Validate sync direction if (!in_array($mapping_data['sync_direction'], ['perfex_to_moloni', 'moloni_to_perfex', 'bidirectional'])) { throw new Exception(_l('desk_moloni_invalid_sync_direction')); } // Validate entities exist if (!$this->_validate_perfex_entity($mapping_data['entity_type'], $mapping_data['perfex_id'])) { throw new Exception(_l('desk_moloni_perfex_entity_not_found')); } if (!$this->_validate_moloni_entity($mapping_data['entity_type'], $mapping_data['moloni_id'])) { throw new Exception(_l('desk_moloni_moloni_entity_not_found')); } // Check for existing mappings if ($this->mapping_model->mapping_exists($mapping_data['entity_type'], $mapping_data['perfex_id'], 'perfex')) { throw new Exception(_l('desk_moloni_perfex_mapping_exists')); } if ($this->mapping_model->mapping_exists($mapping_data['entity_type'], $mapping_data['moloni_id'], 'moloni')) { throw new Exception(_l('desk_moloni_moloni_mapping_exists')); } $mapping_id = $this->mapping_model->create_mapping($mapping_data); $this->output ->set_content_type('application/json') ->set_output(json_encode([ 'success' => true, 'message' => _l('desk_moloni_mapping_created_successfully'), 'data' => ['mapping_id' => $mapping_id] ])); } catch (Exception $e) { log_message('error', 'Desk-Moloni create mapping error: ' . $e->getMessage()); $this->output ->set_status_header(400) ->set_content_type('application/json') ->set_output(json_encode([ 'success' => false, 'message' => $e->getMessage() ])); } } /** * Update mapping */ public function update_mapping($mapping_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 { $mapping_id = (int) $mapping_id; if (!$mapping_id) { throw new Exception(_l('desk_moloni_invalid_mapping_id')); } $update_data = []; // Only allow updating sync_direction if ($this->input->post('sync_direction') !== null) { $sync_direction = $this->input->post('sync_direction'); if (!in_array($sync_direction, ['perfex_to_moloni', 'moloni_to_perfex', 'bidirectional'])) { throw new Exception(_l('desk_moloni_invalid_sync_direction')); } $update_data['sync_direction'] = $sync_direction; } if (empty($update_data)) { throw new Exception(_l('desk_moloni_no_update_data')); } $result = $this->mapping_model->update_mapping($mapping_id, $update_data); if (!$result) { throw new Exception(_l('desk_moloni_mapping_update_failed')); } $this->output ->set_content_type('application/json') ->set_output(json_encode([ 'success' => true, 'message' => _l('desk_moloni_mapping_updated_successfully') ])); } catch (Exception $e) { log_message('error', 'Desk-Moloni update mapping error: ' . $e->getMessage()); $this->output ->set_status_header(400) ->set_content_type('application/json') ->set_output(json_encode([ 'success' => false, 'message' => $e->getMessage() ])); } } /** * Delete mapping */ public function delete_mapping($mapping_id) { 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 { $mapping_id = (int) $mapping_id; if (!$mapping_id) { throw new Exception(_l('desk_moloni_invalid_mapping_id')); } $result = $this->mapping_model->delete_mapping($mapping_id); if (!$result) { throw new Exception(_l('desk_moloni_mapping_delete_failed')); } $this->output ->set_content_type('application/json') ->set_output(json_encode([ 'success' => true, 'message' => _l('desk_moloni_mapping_deleted_successfully') ])); } catch (Exception $e) { log_message('error', 'Desk-Moloni delete mapping error: ' . $e->getMessage()); $this->output ->set_status_header(400) ->set_content_type('application/json') ->set_output(json_encode([ 'success' => false, 'message' => $e->getMessage() ])); } } /** * Bulk mapping operations */ 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'); $mapping_ids = $this->input->post('mapping_ids'); if (empty($operation) || empty($mapping_ids) || !is_array($mapping_ids)) { throw new Exception(_l('desk_moloni_bulk_operation_invalid_params')); } $results = []; $success_count = 0; $error_count = 0; foreach ($mapping_ids as $mapping_id) { try { $mapping_id = (int) $mapping_id; $result = false; switch ($operation) { case 'delete': $result = $this->mapping_model->delete_mapping($mapping_id); break; case 'sync_perfex_to_moloni': $result = $this->mapping_model->update_mapping($mapping_id, ['sync_direction' => 'perfex_to_moloni']); break; case 'sync_moloni_to_perfex': $result = $this->mapping_model->update_mapping($mapping_id, ['sync_direction' => 'moloni_to_perfex']); break; case 'sync_bidirectional': $result = $this->mapping_model->update_mapping($mapping_id, ['sync_direction' => 'bidirectional']); break; default: throw new Exception(_l('desk_moloni_invalid_bulk_operation')); } if ($result) { $success_count++; $results[$mapping_id] = ['success' => true]; } else { $error_count++; $results[$mapping_id] = ['success' => false, 'error' => 'Operation failed']; } } catch (Exception $e) { $error_count++; $results[$mapping_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 mapping operation error: ' . $e->getMessage()); $this->output ->set_status_header(400) ->set_content_type('application/json') ->set_output(json_encode([ 'success' => false, 'message' => $e->getMessage() ])); } } /** * Auto-discover and suggest mappings */ public function auto_discover() { if (!has_permission('desk_moloni_admin', '', 'create')) { $this->output ->set_status_header(403) ->set_content_type('application/json') ->set_output(json_encode(['success' => false, 'message' => _l('access_denied')])); return; } try { $entity_type = $this->input->post('entity_type'); $auto_create = $this->input->post('auto_create') === '1'; if (empty($entity_type)) { throw new Exception(_l('desk_moloni_entity_type_required')); } $this->load->library('desk_moloni/entity_mapping_service'); $suggestions = $this->entity_mapping_service->discover_mappings($entity_type); $created_count = 0; if ($auto_create && !empty($suggestions)) { foreach ($suggestions as $suggestion) { try { $this->mapping_model->create_mapping([ 'entity_type' => $entity_type, 'perfex_id' => $suggestion['perfex_id'], 'moloni_id' => $suggestion['moloni_id'], 'sync_direction' => 'bidirectional' ]); $created_count++; } catch (Exception $e) { // Continue with other suggestions if one fails log_message('warning', 'Auto-create mapping failed: ' . $e->getMessage()); } } } $this->output ->set_content_type('application/json') ->set_output(json_encode([ 'success' => true, 'message' => sprintf( _l('desk_moloni_auto_discover_results'), count($suggestions), $created_count ), 'data' => [ 'suggestions' => $suggestions, 'created_count' => $created_count ] ])); } catch (Exception $e) { log_message('error', 'Desk-Moloni auto discover error: ' . $e->getMessage()); $this->output ->set_status_header(500) ->set_content_type('application/json') ->set_output(json_encode([ 'success' => false, 'message' => $e->getMessage() ])); } } /** * Get entity suggestions for mapping creation */ public function get_entity_suggestions() { 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 { $entity_type = $this->input->get('entity_type'); $system = $this->input->get('system'); // 'perfex' or 'moloni' $search = $this->input->get('search'); $limit = (int) $this->input->get('limit') ?: 20; if (empty($entity_type) || empty($system)) { throw new Exception(_l('desk_moloni_missing_parameters')); } $suggestions = []; if ($system === 'perfex') { $suggestions = $this->_get_perfex_entity_suggestions($entity_type, $search, $limit); } else { $suggestions = $this->_get_moloni_entity_suggestions($entity_type, $search, $limit); } $this->output ->set_content_type('application/json') ->set_output(json_encode([ 'success' => true, 'data' => $suggestions ])); } catch (Exception $e) { log_message('error', 'Desk-Moloni entity suggestions error: ' . $e->getMessage()); $this->output ->set_status_header(500) ->set_content_type('application/json') ->set_output(json_encode([ 'success' => false, 'message' => $e->getMessage() ])); } } /** * Get entity name for display */ private function _get_entity_name($system, $entity_type, $entity_id) { try { if ($system === 'perfex') { return $this->_get_perfex_entity_name($entity_type, $entity_id); } else { return $this->_get_moloni_entity_name($entity_type, $entity_id); } } catch (Exception $e) { return "ID: $entity_id"; } } /** * Get Perfex entity name */ private function _get_perfex_entity_name($entity_type, $entity_id) { switch ($entity_type) { case 'client': $this->load->model('clients_model'); $client = $this->clients_model->get($entity_id); return $client ? $client->company : "Client #$entity_id"; case 'product': $this->load->model('items_model'); $item = $this->items_model->get($entity_id); return $item ? $item->description : "Product #$entity_id"; case 'invoice': $this->load->model('invoices_model'); $invoice = $this->invoices_model->get($entity_id); return $invoice ? format_invoice_number($invoice->id) : "Invoice #$entity_id"; case 'estimate': $this->load->model('estimates_model'); $estimate = $this->estimates_model->get($entity_id); return $estimate ? format_estimate_number($estimate->id) : "Estimate #$entity_id"; case 'credit_note': $this->load->model('credit_notes_model'); $credit_note = $this->credit_notes_model->get($entity_id); return $credit_note ? format_credit_note_number($credit_note->id) : "Credit Note #$entity_id"; default: return "Entity #$entity_id"; } } /** * Get Moloni entity name */ private function _get_moloni_entity_name($entity_type, $entity_id) { try { $this->load->library('desk_moloni/moloni_api_client'); $entity_data = $this->moloni_api_client->get_entity($entity_type, $entity_id); switch ($entity_type) { case 'client': return $entity_data['name'] ?? "Client #$entity_id"; case 'product': return $entity_data['name'] ?? "Product #$entity_id"; case 'invoice': return $entity_data['document_set_name'] . ' ' . $entity_data['number'] ?? "Invoice #$entity_id"; case 'estimate': return $entity_data['document_set_name'] . ' ' . $entity_data['number'] ?? "Estimate #$entity_id"; case 'credit_note': return $entity_data['document_set_name'] . ' ' . $entity_data['number'] ?? "Credit Note #$entity_id"; default: return "Entity #$entity_id"; } } catch (Exception $e) { return "Entity #$entity_id"; } } /** * Validate Perfex entity exists */ private function _validate_perfex_entity($entity_type, $entity_id) { switch ($entity_type) { case 'client': $this->load->model('clients_model'); return $this->clients_model->get($entity_id) !== false; case 'product': $this->load->model('items_model'); return $this->items_model->get($entity_id) !== false; case 'invoice': $this->load->model('invoices_model'); return $this->invoices_model->get($entity_id) !== false; case 'estimate': $this->load->model('estimates_model'); return $this->estimates_model->get($entity_id) !== false; case 'credit_note': $this->load->model('credit_notes_model'); return $this->credit_notes_model->get($entity_id) !== false; default: return false; } } /** * Validate Moloni entity exists */ private function _validate_moloni_entity($entity_type, $entity_id) { try { $this->load->library('desk_moloni/moloni_api_client'); $entity = $this->moloni_api_client->get_entity($entity_type, $entity_id); return !empty($entity); } catch (Exception $e) { return false; } } /** * Get Perfex entity suggestions */ private function _get_perfex_entity_suggestions($entity_type, $search = '', $limit = 20) { // Implementation depends on specific models and search requirements // This is a simplified version $suggestions = []; switch ($entity_type) { case 'client': $this->load->model('clients_model'); // Get clients with search filter $clients = $this->clients_model->get('', ['limit' => $limit]); foreach ($clients as $client) { $suggestions[] = [ 'id' => $client['userid'], 'name' => $client['company'] ]; } break; // Add other entity types as needed } return $suggestions; } /** * Get Moloni entity suggestions */ private function _get_moloni_entity_suggestions($entity_type, $search = '', $limit = 20) { try { $this->load->library('desk_moloni/moloni_api_client'); return $this->moloni_api_client->search_entities($entity_type, $search, $limit); } catch (Exception $e) { return []; } } }