Files
desk-moloni/modules/desk_moloni/desk_moloni.php
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 01:27:37 +01:00

554 lines
17 KiB
PHP

/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Desk-Moloni v3.0 - Perfex CRM Module
*
* Bidirectional synchronization between Perfex CRM and Moloni ERP
*
* @package DeskMoloni
* @author Descomplicar.pt
* @version 3.0.1
* @link https://descomplicar.pt
*/
defined('BASEPATH') or exit('No direct script access allowed');
/*
Module Name: Desk-Moloni Integration v3.0
Description: Complete bidirectional synchronization between Perfex CRM and Moloni ERP with OAuth 2.0, queue processing, and client portal
Version: 3.0.1
Requires at least: 3.0.*
Author: Descomplicar.pt
Author URI: https://descomplicar.pt
*/
// Define constants only if they don't already exist
if (!defined('DESK_MOLONI_MODULE_NAME')) {
define('DESK_MOLONI_MODULE_NAME', 'desk_moloni');
}
if (!defined('DESK_MOLONI_VERSION')) {
define('DESK_MOLONI_VERSION', '3.0.1');
}
if (!defined('DESK_MOLONI_MODULE_PATH')) {
define('DESK_MOLONI_MODULE_PATH', dirname(__FILE__));
}
// Load Composer autoloader
if (file_exists(DESK_MOLONI_MODULE_PATH . '/vendor/autoload.php')) {
require_once DESK_MOLONI_MODULE_PATH . '/vendor/autoload.php';
}
/**
* Register hooks for module functionality - only if hooks() function exists
*/
if (function_exists('hooks')) {
hooks()->add_action('after_client_added', 'desk_moloni_sync_customer_added');
hooks()->add_action('after_client_updated', 'desk_moloni_sync_customer_updated');
hooks()->add_action('after_invoice_added', 'desk_moloni_sync_invoice_added');
hooks()->add_action('after_invoice_updated', 'desk_moloni_sync_invoice_updated');
hooks()->add_action('after_estimate_added', 'desk_moloni_sync_estimate_added');
hooks()->add_action('after_estimate_updated', 'desk_moloni_sync_estimate_updated');
hooks()->add_action('after_item_added', 'desk_moloni_sync_item_added');
hooks()->add_action('after_item_updated', 'desk_moloni_sync_item_updated');
/**
* Register admin menu hooks
*/
hooks()->add_action('admin_init', 'desk_moloni_admin_init_hook');
hooks()->add_action('admin_init', 'desk_moloni_init_admin_menu');
/**
* Register client portal hooks
*/
hooks()->add_action('client_init', 'desk_moloni_client_init_hook');
}
// Optionally initialize the advanced PerfexHooks class if available
if (class_exists('PerfexHooks') && function_exists('hooks')) {
// Instantiate once to register internal handlers (safe to try)
try { new PerfexHooks(); } catch (Throwable $e) { /* ignore */ }
}
/**
* Module lifecycle hooks - only register if functions exist
*/
if (function_exists('register_activation_hook')) {
register_activation_hook(DESK_MOLONI_MODULE_NAME, 'desk_moloni_activation_hook');
}
if (function_exists('register_deactivation_hook')) {
register_deactivation_hook(DESK_MOLONI_MODULE_NAME, 'desk_moloni_deactivation_hook');
}
if (function_exists('register_uninstall_hook')) {
register_uninstall_hook(DESK_MOLONI_MODULE_NAME, 'desk_moloni_uninstall_hook');
}
/**
* Hook functions
*/
function desk_moloni_admin_init_hook()
{
$CI = &get_instance();
// Load module configuration safely
try {
$config_file = DESK_MOLONI_MODULE_PATH . '/config/config.php';
if (file_exists($config_file)) {
include_once $config_file;
}
} catch (Exception $e) {
log_message('error', 'Desk-Moloni: Failed to load config: ' . $e->getMessage());
}
// Ensure version option is up to date
if (function_exists('update_option')) {
update_option('desk_moloni_module_version', DESK_MOLONI_VERSION);
}
// Add CSS and JS for admin if files exist
$css_file = DESK_MOLONI_MODULE_PATH . '/assets/css/admin.css';
$js_file = DESK_MOLONI_MODULE_PATH . '/assets/js/admin.js';
if (file_exists($css_file)) {
$CI->app_css->add('desk-moloni-admin-css', base_url('modules/desk_moloni/assets/css/admin.css'));
}
if (file_exists($js_file)) {
$CI->app_scripts->add('desk-moloni-admin-js', base_url('modules/desk_moloni/assets/js/admin.js'));
}
}
function desk_moloni_init_admin_menu()
{
$CI = &get_instance();
if (has_permission('desk_moloni', '', 'view')) {
$CI->app_menu->add_sidebar_menu_item('desk-moloni', [
'name' => 'Desk-Moloni',
'href' => admin_url('desk_moloni/admin'),
'icon' => 'fa fa-refresh',
'position' => 35,
]);
$CI->app_menu->add_sidebar_children_item('desk-moloni', [
'slug' => 'desk-moloni-dashboard',
'name' => _l('Dashboard'),
'href' => admin_url('desk_moloni/dashboard'),
'position' => 1,
]);
$CI->app_menu->add_sidebar_children_item('desk-moloni', [
'slug' => 'desk-moloni-config',
'name' => _l('Configuration'),
'href' => admin_url('desk_moloni/admin/config'),
'position' => 2,
]);
$CI->app_menu->add_sidebar_children_item('desk-moloni', [
'slug' => 'desk-moloni-sync',
'name' => _l('Synchronization'),
'href' => admin_url('desk_moloni/admin/manual_sync'),
'position' => 3,
]);
$CI->app_menu->add_sidebar_children_item('desk-moloni', [
'slug' => 'desk-moloni-queue',
'name' => _l('Queue Status'),
'href' => admin_url('desk_moloni/queue'),
'position' => 4,
]);
$CI->app_menu->add_sidebar_children_item('desk-moloni', [
'slug' => 'desk-moloni-mapping',
'name' => _l('Mappings'),
'href' => admin_url('desk_moloni/mapping'),
'position' => 5,
]);
$CI->app_menu->add_sidebar_children_item('desk-moloni', [
'slug' => 'desk-moloni-logs',
'name' => _l('Sync Logs'),
'href' => admin_url('desk_moloni/logs'),
'position' => 6,
]);
}
}
function desk_moloni_client_init_hook()
{
$CI = &get_instance();
// Add client portal CSS and JS only if files exist
$css_path = DESK_MOLONI_MODULE_PATH . '/assets/css/client.css';
if (file_exists($css_path)) {
$CI->app_css->add('desk-moloni-client-css', base_url('modules/desk_moloni/assets/css/client.css'));
}
// Skip non-existent JS file to avoid 404 errors
// Client portal JavaScript will be loaded when the frontend is properly built
// Add client portal tab if helper exists
if (function_exists('hooks')) {
hooks()->add_action('clients_navigation_end', 'desk_moloni_add_client_tab');
}
}
function desk_moloni_add_client_tab()
{
$CI = &get_instance();
echo '<li role="presentation">';
echo '<a href="' . site_url('clients/desk_moloni') . '" aria-controls="desk_moloni" role="tab" data-toggle="tab">';
echo '<i class="fa fa-file-text-o"></i> ' . _l('My Documents');
echo '</a>';
echo '</li>';
}
/**
* Synchronization hook functions
*/
function desk_moloni_sync_customer_added($customer_id)
{
desk_moloni_add_sync_task('sync_client', 'client', $customer_id);
}
function desk_moloni_sync_customer_updated($customer_id)
{
desk_moloni_add_sync_task('sync_client', 'client', $customer_id);
}
function desk_moloni_sync_invoice_added($invoice_id)
{
desk_moloni_add_sync_task('sync_invoice', 'invoice', $invoice_id);
}
function desk_moloni_sync_invoice_updated($invoice_id)
{
desk_moloni_add_sync_task('sync_invoice', 'invoice', $invoice_id);
}
function desk_moloni_sync_estimate_added($estimate_id)
{
desk_moloni_add_sync_task('sync_estimate', 'estimate', $estimate_id);
}
function desk_moloni_sync_estimate_updated($estimate_id)
{
desk_moloni_add_sync_task('sync_estimate', 'estimate', $estimate_id);
}
function desk_moloni_sync_item_added($item_id)
{
desk_moloni_add_sync_task('sync_product', 'product', $item_id);
}
function desk_moloni_sync_item_updated($item_id)
{
desk_moloni_add_sync_task('sync_product', 'product', $item_id);
}
/**
* Add task to sync queue
*/
function desk_moloni_add_sync_task($task_type, $entity_type, $entity_id, $priority = 5)
{
$CI = &get_instance();
// Check if sync is enabled
$sync_enabled = get_option('desk_moloni_sync_enabled');
if (!$sync_enabled) {
return false;
}
// Check if specific entity sync is enabled
$entity_sync_key = 'desk_moloni_auto_sync_' . $entity_type . 's';
$entity_sync_enabled = get_option($entity_sync_key);
if (!$entity_sync_enabled) {
return false;
}
// Load sync queue model with correct path and alias
$CI->load->model('desk_moloni/desk_moloni_sync_queue_model', 'sync_queue_model');
// Add task to queue
if (method_exists($CI->sync_queue_model, 'addTask')) {
return $CI->sync_queue_model->addTask($task_type, $entity_type, $entity_id, [], $priority);
}
if (method_exists($CI->sync_queue_model, 'add_task')) {
return $CI->sync_queue_model->add_task([
'task_type' => $task_type,
'entity_type' => $entity_type,
'entity_id' => $entity_id,
'priority' => $priority,
]);
}
return false;
}
/**
* Module lifecycle hooks
*/
function desk_moloni_activation_hook()
{
$CI = &get_instance();
try {
// Run database migrations
$migration_success = desk_moloni_run_migrations();
if (!$migration_success) {
log_message('warning', 'Desk-Moloni: Migration failed, but continuing activation');
}
// Create default configuration
desk_moloni_create_default_config();
// Setup permissions
desk_moloni_setup_permissions();
// Load and run install.php for complete setup
$install_file = DESK_MOLONI_MODULE_PATH . '/install.php';
if (file_exists($install_file)) {
include_once $install_file;
}
log_activity('Desk-Moloni module activated successfully');
} catch (Exception $e) {
log_message('error', 'Desk-Moloni activation error: ' . $e->getMessage());
log_activity('Desk-Moloni module activation failed: ' . $e->getMessage());
}
}
function desk_moloni_deactivation_hook()
{
log_activity('Desk-Moloni module deactivated');
}
function desk_moloni_uninstall_hook()
{
$CI = &get_instance();
// Remove module data (optional - admin choice)
$remove_data = get_option('desk_moloni_remove_data_on_uninstall');
if ($remove_data) {
desk_moloni_remove_database_tables();
desk_moloni_remove_module_options();
desk_moloni_remove_permissions();
}
log_activity('Desk-Moloni module uninstalled');
}
/**
* Database migration functions
*/
function desk_moloni_run_migrations()
{
$CI = &get_instance();
try {
$CI->load->database();
$migration_path = DESK_MOLONI_MODULE_PATH . '/database/migrations/';
// Check if migrations directory exists
if (!is_dir($migration_path)) {
log_message('info', 'Desk-Moloni: No migrations directory found, using install.php for database setup');
return true;
}
// Get all migration files
$migrations = glob($migration_path . '*.sql');
if (empty($migrations)) {
log_message('info', 'Desk-Moloni: No migration files found, using install.php for database setup');
return true;
}
sort($migrations);
// Ensure migrations table exists
$table_name = db_prefix() . 'desk_moloni_migrations';
if (!$CI->db->table_exists($table_name)) {
$CI->db->query("CREATE TABLE IF NOT EXISTS `{$table_name}` (
id INT AUTO_INCREMENT PRIMARY KEY,
migration VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
}
foreach ($migrations as $migration_file) {
$migration_name = basename($migration_file, '.sql');
// Check if this specific migration has been run
$executed = $CI->db->get_where($table_name, ['migration' => $migration_name])->num_rows();
if ($executed == 0) {
// Execute migration
$sql = file_get_contents($migration_file);
if (!empty($sql)) {
$queries = explode(';', $sql);
foreach ($queries as $query) {
$query = trim($query);
if (!empty($query) && strlen($query) > 10) {
try {
$CI->db->query($query);
} catch (Exception $e) {
log_message('error', 'Desk-Moloni migration error in ' . $migration_name . ': ' . $e->getMessage());
// Continue with other queries
}
}
}
// Record migration as executed
$CI->db->insert($table_name, [
'migration' => $migration_name,
'executed_at' => date('Y-m-d H:i:s')
]);
log_activity("Desk-Moloni migration executed: {$migration_name}");
}
}
}
return true;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni migration error: ' . $e->getMessage());
return false;
}
}
function desk_moloni_create_default_config()
{
$default_options = [
'desk_moloni_sync_enabled' => '0',
'desk_moloni_auto_sync_clients' => '1',
'desk_moloni_auto_sync_products' => '0',
'desk_moloni_auto_sync_invoices' => '1',
'desk_moloni_auto_sync_estimates' => '1',
'desk_moloni_queue_processing_enabled' => '1',
'desk_moloni_max_retry_attempts' => '3',
'desk_moloni_sync_timeout' => '30',
'desk_moloni_remove_data_on_uninstall' => '0'
];
foreach ($default_options as $key => $value) {
if (get_option($key) === false) {
add_option($key, $value);
}
}
}
function desk_moloni_setup_permissions()
{
$CI = &get_instance();
// Check if permission already exists
$permission_exists = $CI->db->get_where('tblpermissions', ['name' => 'desk_moloni'])->num_rows();
if ($permission_exists == 0) {
// Add module permissions
$permissions = [
'view' => 'View Desk-Moloni',
'create' => 'Create Sync Tasks',
'edit' => 'Edit Configuration',
'delete' => 'Delete Sync Tasks'
];
foreach ($permissions as $short_name => $description) {
$CI->db->insert('tblpermissions', [
'name' => 'desk_moloni',
'shortname' => $short_name,
'description' => $description
]);
}
}
}
function desk_moloni_remove_database_tables()
{
$CI = &get_instance();
$CI->load->database();
// Drop both legacy and current table names for safety
$legacyTables = [
'desk_moloni_config',
'desk_moloni_mapping',
'desk_moloni_sync_queue',
'desk_moloni_sync_log',
'desk_moloni_migrations'
];
$currentTables = [
'tbldeskmoloni_config',
'tbldeskmoloni_mapping',
'tbldeskmoloni_sync_queue',
'tbldeskmoloni_sync_log',
'tbldeskmoloni_migrations'
];
foreach (array_merge($legacyTables, $currentTables) as $table) {
$CI->db->query("DROP TABLE IF EXISTS {$table}");
}
}
function desk_moloni_remove_module_options()
{
$CI = &get_instance();
// Remove all module options
$CI->db->like('name', 'desk_moloni_', 'after');
$CI->db->delete('tbloptions');
}
function desk_moloni_remove_permissions()
{
$CI = &get_instance();
// Remove module permissions
$CI->db->where('name', 'desk_moloni');
$CI->db->delete('tblpermissions');
}
/**
* Client portal route handler
*/
function desk_moloni_client_portal_route()
{
$CI = &get_instance();
// Check if client is logged in
if (!is_client_logged_in()) {
redirect("clients/login");
return;
}
// Load the client portal view
$CI->load->view("desk_moloni/client_portal/index");
}
/**
* Register client portal routes
*/
if (function_exists('hooks')) {
hooks()->add_action("clients_init", function() {
$CI = &get_instance();
// Register the main client portal route if router is available
if (isset($CI->router) && property_exists($CI->router, 'route') && is_array($CI->router->route)) {
$CI->router->route["clients/desk_moloni"] = "desk_moloni_client_portal_route";
}
});
}