- 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>
554 lines
17 KiB
PHP
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";
|
|
}
|
|
});
|
|
}
|