diff --git a/wordpress-dev/bin/deploy_config.sh b/wordpress-dev/bin/deploy_config.sh
index 447a1ad6..537b032b 100755
--- a/wordpress-dev/bin/deploy_config.sh
+++ b/wordpress-dev/bin/deploy_config.sh
@@ -1,12 +1,50 @@
#!/bin/bash
# Load environment variables from .env
-source ../.env
+if [ -f "$(dirname "$0")/../../.env" ]; then
+ source "$(dirname "$0")/../../.env"
+elif [ -f ".env" ]; then
+ source ".env"
+else
+ echo "Error: .env file not found"
+ exit 1
+fi
# Define deployment variables
REMOTE_HOST="$UPSKILL_STAGING_IP"
REMOTE_USER="$UPSKILL_STAGING_SSH_USER"
REMOTE_PATH_BASE="$UPSKILL_STAGING_PATH"
PLUGIN_SLUG="hvac-community-events"
-REMOTE_PLUGIN_PATH="$REMOTE_PATH_BASE/wp-content/plugins/$PLUGIN_SLUG"
-LOCAL_PLUGIN_PATH="wordpress-dev/wordpress/wp-content/plugins/$PLUGIN_SLUG"
\ No newline at end of file
+
+# Define files/directories to exclude
+EXCLUDE_PATTERNS=(
+ ".git/"
+ ".gitignore"
+ "node_modules/"
+ ".DS_Store"
+ "*.log"
+ ".env"
+ ".env.local"
+ "*.swp"
+ "*.tmp"
+ "tests/coverage/"
+ "tests/bin/"
+ "wordpress-dev/"
+ "*.md"
+ "composer.lock"
+ "package-lock.json"
+)
+
+# Define which directories to deploy
+DEPLOY_DIRS=(
+ "includes"
+ "templates"
+ "assets"
+)
+
+# Define which root files to deploy
+DEPLOY_FILES=(
+ "hvac-community-events.php"
+ "README.txt"
+ "index.php"
+)
\ No newline at end of file
diff --git a/wordpress-dev/bin/disable-breeze-cache-testing.sh b/wordpress-dev/bin/disable-breeze-cache-testing.sh
new file mode 100755
index 00000000..e6ff2aad
--- /dev/null
+++ b/wordpress-dev/bin/disable-breeze-cache-testing.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+
+# Script to disable Breeze cache for testing environments
+# This creates an mu-plugin that sets DONOTCACHEPAGE constant
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+
+# Source environment variables
+if [ -f "$PROJECT_ROOT/.env" ]; then
+ source "$PROJECT_ROOT/.env"
+fi
+
+# Check for required environment variables
+if [ -z "$UPSKILL_STAGING_SSH_USER" ] || [ -z "$UPSKILL_STAGING_PASS" ] || [ -z "$UPSKILL_STAGING_IP" ] || [ -z "$UPSKILL_STAGING_PATH" ]; then
+ echo "Error: Missing required environment variables."
+ echo "Please ensure the following are set in your .env file:"
+ echo " - UPSKILL_STAGING_SSH_USER"
+ echo " - UPSKILL_STAGING_PASS"
+ echo " - UPSKILL_STAGING_IP"
+ echo " - UPSKILL_STAGING_PATH"
+ exit 1
+fi
+
+echo "Creating mu-plugin to disable Breeze cache for testing..."
+
+# Create the mu-plugin content
+MU_PLUGIN_CONTENT=' wp-content/mu-plugins/disable-breeze-for-tests.php << 'EOF'
+$MU_PLUGIN_CONTENT
+EOF"
+
+# Verify the file was created
+echo "Verifying mu-plugin creation..."
+FILE_EXISTS=$(sshpass -p "${UPSKILL_STAGING_PASS}" ssh -o StrictHostKeyChecking=no "${UPSKILL_STAGING_SSH_USER}@${UPSKILL_STAGING_IP}" \
+"cd ${UPSKILL_STAGING_PATH} && [ -f wp-content/mu-plugins/disable-breeze-for-tests.php ] && echo 'exists' || echo 'not found'")
+
+if [ "$FILE_EXISTS" = "exists" ]; then
+ echo "✅ Successfully created mu-plugin to disable Breeze cache for testing"
+ echo "The following conditions will disable cache:"
+ echo " - WP_ENV environment variable is set to 'testing'"
+ echo " - WP_TESTS_DOMAIN constant is defined"
+ echo " - User agent contains 'Playwright'"
+ echo " - URL has 'no_cache_test' query parameter"
+ echo " - URL contains '/manage-event/'"
+else
+ echo "❌ Failed to create mu-plugin"
+ exit 1
+fi
+
+# Clear existing cache
+echo "Clearing existing Breeze cache..."
+$SCRIPT_DIR/clear-breeze-cache.sh
+
+echo "✅ Breeze cache setup for testing complete"
\ No newline at end of file
diff --git a/wordpress-dev/bin/test-zoho-integration.sh b/wordpress-dev/bin/test-zoho-integration.sh
new file mode 100755
index 00000000..6770ede6
--- /dev/null
+++ b/wordpress-dev/bin/test-zoho-integration.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+# Zoho CRM Integration Test Script
+# This script tests the Zoho integration setup
+
+# Color codes for output
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+# Get the script directory
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+ZOHO_DIR="$PROJECT_ROOT/wordpress/wp-content/plugins/hvac-community-events/includes/zoho"
+
+echo -e "${GREEN}=== Zoho CRM Integration Test ===${NC}\n"
+
+# Check if .env file exists
+if [ ! -f "$PROJECT_ROOT/.env" ]; then
+ echo -e "${RED}Error: .env file not found at $PROJECT_ROOT/.env${NC}"
+ exit 1
+fi
+
+# Source the .env file
+source "$PROJECT_ROOT/.env"
+
+# Check if credentials exist
+if [ -z "$ZOHO_CLIENT_ID" ] || [ -z "$ZOHO_CLIENT_SECRET" ]; then
+ echo -e "${RED}Error: ZOHO_CLIENT_ID or ZOHO_CLIENT_SECRET not found in .env file${NC}"
+ exit 1
+fi
+
+echo -e "${GREEN}✓ Credentials found in .env file${NC}"
+echo -e "Client ID: ${ZOHO_CLIENT_ID:0:20}...\n"
+
+# Change to Zoho directory
+cd "$ZOHO_DIR"
+
+# Check if PHP is available
+if ! command -v php &> /dev/null; then
+ echo -e "${RED}Error: PHP is not installed${NC}"
+ exit 1
+fi
+
+echo -e "${YELLOW}Starting Zoho integration test...${NC}\n"
+
+# Option to start auth server
+echo -e "${YELLOW}Would you like to start the OAuth callback server? (y/n)${NC}"
+read -p "This will help capture the authorization code automatically: " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo -e "\n${GREEN}Starting OAuth callback server...${NC}"
+ echo -e "${YELLOW}Keep this terminal open and start a new terminal for the next step.${NC}\n"
+ php "$ZOHO_DIR/auth-server.php"
+else
+ echo -e "\n${GREEN}Running test integration script...${NC}"
+ php "$ZOHO_DIR/test-integration.php"
+fi
+
+echo -e "\n${GREEN}=== Test Complete ===${NC}"
\ No newline at end of file
diff --git a/wordpress-dev/bin/zoho-oauth-setup.sh b/wordpress-dev/bin/zoho-oauth-setup.sh
new file mode 100755
index 00000000..e88083bc
--- /dev/null
+++ b/wordpress-dev/bin/zoho-oauth-setup.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# Zoho OAuth Setup Helper Script
+set -e
+
+# Colors for output
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+echo -e "${BLUE}=== Zoho OAuth Setup Helper ===${NC}"
+echo
+echo -e "${YELLOW}This script will guide you through the Zoho OAuth setup process.${NC}"
+echo
+echo -e "${GREEN}Step 1: Open Authorization URL${NC}"
+echo "Open the following URL in your browser:"
+echo
+echo "https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCRM.settings.all%2CZohoCRM.modules.all%2CZohoCRM.users.all%2CZohoCRM.org.all&client_id=1000.Z0HOF1VMMJ9W2QWSU57GVQYEAVUSKS&response_type=code&access_type=offline&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&prompt=consent"
+echo
+echo -e "${GREEN}Step 2: Authorize and Get Code${NC}"
+echo "1. Log in to Zoho if prompted"
+echo "2. Review and accept the permissions"
+echo "3. You'll be redirected to: http://localhost:8080/callback?code=AUTH_CODE"
+echo "4. Copy the 'code' parameter from the URL"
+echo
+echo -e "${GREEN}Step 3: Run Integration Test${NC}"
+echo "Once you have the code, run:"
+echo "cd /Users/ben/dev/upskill-event-manager/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho"
+echo "php test-integration.php"
+echo
+echo "Then paste the authorization code when prompted."
+echo
+echo -e "${BLUE}Note: The authorization code expires quickly, so complete the process promptly.${NC}"
\ No newline at end of file
diff --git a/wordpress-dev/bin/zoho-setup-complete.sh b/wordpress-dev/bin/zoho-setup-complete.sh
new file mode 100755
index 00000000..a55dd3db
--- /dev/null
+++ b/wordpress-dev/bin/zoho-setup-complete.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+
+# Complete Zoho OAuth Setup Script
+set -e
+
+# Colors for output
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+echo -e "${BLUE}=== Complete Zoho OAuth Setup ===${NC}"
+echo
+
+PLUGIN_DIR="/Users/ben/dev/upskill-event-manager/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events"
+ZOHO_DIR="$PLUGIN_DIR/includes/zoho"
+
+# Step 1: Start callback server
+echo -e "${GREEN}Starting callback server...${NC}"
+cd "$ZOHO_DIR"
+php -S localhost:8080 callback-server.php &
+SERVER_PID=$!
+echo "Callback server started (PID: $SERVER_PID)"
+echo
+
+# Step 2: Display authorization URL
+echo -e "${GREEN}Please complete authorization:${NC}"
+echo
+echo -e "${YELLOW}1. Open this URL in your browser:${NC}"
+echo
+echo "https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCRM.settings.all%2CZohoCRM.modules.all%2CZohoCRM.users.all%2CZohoCRM.org.all&client_id=1000.Z0HOF1VMMJ9W2QWSU57GVQYEAVUSKS&response_type=code&access_type=offline&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&prompt=consent"
+echo
+echo -e "${YELLOW}2. Log in to Zoho and accept permissions${NC}"
+echo -e "${YELLOW}3. Copy the authorization code from the callback page${NC}"
+echo -e "${YELLOW}4. Return here and paste the code${NC}"
+echo
+
+# Step 3: Wait for callback
+echo -e "${GREEN}Waiting for authorization...${NC}"
+echo "Once you've authorized, you'll see the code at: http://localhost:8080/callback"
+echo
+read -p "Enter the authorization code: " AUTH_CODE
+
+# Step 4: Stop callback server
+kill $SERVER_PID 2>/dev/null || true
+echo
+
+# Step 5: Run integration test with the code
+echo -e "${GREEN}Testing integration with authorization code...${NC}"
+cd "$ZOHO_DIR"
+echo "$AUTH_CODE" | php test-integration.php
+
+echo
+echo -e "${GREEN}✓ Setup complete!${NC}"
+echo
+echo "The Zoho configuration has been saved to:"
+echo "$ZOHO_DIR/zoho-config.php"
+echo
+echo "You can now sync data to Zoho CRM!"
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore
new file mode 100644
index 00000000..2ad4b848
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/.gitignore
@@ -0,0 +1,10 @@
+# Ignore Zoho credentials
+includes/zoho/zoho-config.php
+
+# Ignore log files
+*.log
+
+# Development files
+.DS_Store
+node_modules/
+vendor/
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/zoho-admin.css b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/zoho-admin.css
new file mode 100644
index 00000000..f429fce3
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/zoho-admin.css
@@ -0,0 +1,56 @@
+/**
+ * Zoho CRM Admin Styles
+ */
+
+.hvac-zoho-status,
+.hvac-zoho-sync,
+.hvac-zoho-settings {
+ margin-top: 30px;
+ background: #fff;
+ padding: 20px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+.sync-section {
+ margin-bottom: 30px;
+ padding-bottom: 30px;
+ border-bottom: 1px solid #eee;
+}
+
+.sync-section:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+}
+
+.sync-section h3 {
+ margin-top: 0;
+}
+
+.sync-status {
+ margin-top: 10px;
+}
+
+.sync-status .notice {
+ margin: 10px 0;
+}
+
+#connection-status {
+ margin-top: 10px;
+}
+
+#connection-status .notice {
+ margin: 10px 0;
+}
+
+.sync-button {
+ margin-top: 10px;
+}
+
+code {
+ background: #f4f4f4;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Courier New', Courier, monospace;
+}
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/zoho-admin.js b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/zoho-admin.js
new file mode 100644
index 00000000..7d7a53c2
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/zoho-admin.js
@@ -0,0 +1,128 @@
+/**
+ * Zoho CRM Admin JavaScript
+ */
+jQuery(document).ready(function($) {
+ // Test connection
+ $('#test-connection').on('click', function() {
+ var $button = $(this);
+ var $status = $('#connection-status');
+
+ $button.prop('disabled', true).text('Testing...');
+ $status.html('');
+
+ $.ajax({
+ url: hvacZoho.ajaxUrl,
+ method: 'POST',
+ data: {
+ action: 'hvac_zoho_test_connection',
+ nonce: hvacZoho.nonce
+ },
+ success: function(response) {
+ if (response.success) {
+ $status.html('
' + response.data.message + ' (' + response.data.modules + ')
');
+ } else {
+ $status.html('' + response.data.message + ': ' + response.data.error + '
');
+ }
+ },
+ error: function() {
+ $status.html('');
+ },
+ complete: function() {
+ $button.prop('disabled', false).text('Test Connection');
+ }
+ });
+ });
+
+ // Sync data
+ $('.sync-button').on('click', function() {
+ var $button = $(this);
+ var type = $button.data('type');
+ var $status = $('#' + type + '-status');
+
+ $button.prop('disabled', true).text('Syncing...');
+ $status.html('Syncing ' + type + '...
');
+
+ $.ajax({
+ url: hvacZoho.ajaxUrl,
+ method: 'POST',
+ data: {
+ action: 'hvac_zoho_sync_data',
+ type: type,
+ nonce: hvacZoho.nonce
+ },
+ success: function(response) {
+ if (response.success) {
+ var result = response.data;
+ var html = '';
+
+ if (result.staging_mode) {
+ html += '
🔧 STAGING MODE - Simulation Results ';
+ html += '
' + result.message + '
';
+ } else {
+ html += '
Sync completed successfully!
';
+ }
+
+ html += '
' +
+ 'Total records: ' + result.total + ' ' +
+ 'Synced: ' + result.synced + ' ' +
+ 'Failed: ' + result.failed + ' ' +
+ ' ';
+
+ if (result.test_data && result.test_data.length > 0) {
+ html += '
' +
+ 'View test data (first 5 records) ' +
+ '' +
+ JSON.stringify(result.test_data.slice(0, 5), null, 2) +
+ ' ' +
+ ' ';
+ }
+
+ html += '
';
+ $status.html(html);
+ } else {
+ $status.html('' + response.data.message + ': ' + response.data.error + '
');
+ }
+ },
+ error: function() {
+ $status.html('');
+ },
+ complete: function() {
+ $button.prop('disabled', false).text('Sync ' + type.charAt(0).toUpperCase() + type.slice(1));
+ }
+ });
+ });
+
+ // Save settings
+ $('#zoho-settings-form').on('submit', function(e) {
+ e.preventDefault();
+
+ var $form = $(this);
+ var $button = $form.find('button[type="submit"]');
+
+ $button.prop('disabled', true).text('Saving...');
+
+ $.ajax({
+ url: hvacZoho.ajaxUrl,
+ method: 'POST',
+ data: {
+ action: 'hvac_zoho_save_settings',
+ nonce: hvacZoho.nonce,
+ auto_sync: $form.find('input[name="auto_sync"]').is(':checked') ? '1' : '0',
+ sync_frequency: $form.find('select[name="sync_frequency"]').val()
+ },
+ success: function(response) {
+ if (response.success) {
+ alert('Settings saved successfully!');
+ } else {
+ alert('Error saving settings: ' + response.data.message);
+ }
+ },
+ error: function() {
+ alert('Error saving settings');
+ },
+ complete: function() {
+ $button.prop('disabled', false).text('Save Settings');
+ }
+ });
+ });
+});
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/bin/run-tests.sh b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/bin/run-tests.sh
new file mode 100755
index 00000000..ad8c1753
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/bin/run-tests.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+
+# HVAC Community Events Test Runner
+# This script runs the plugin's test suite
+
+# Set up colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Get the directory where this script is located
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
+
+echo -e "${GREEN}HVAC Community Events Test Suite${NC}"
+echo "=================================="
+
+# Check if PHPUnit is available
+if ! command -v phpunit &> /dev/null; then
+ echo -e "${RED}Error: PHPUnit is not installed or not in PATH${NC}"
+ echo "Please install PHPUnit or use the vendor/bin/phpunit"
+ exit 1
+fi
+
+# Change to plugin directory
+cd "$PLUGIN_DIR"
+
+# Run different test suites based on arguments
+if [ "$1" = "unit" ]; then
+ echo -e "${YELLOW}Running Unit Tests...${NC}"
+ phpunit --testsuite=unit
+elif [ "$1" = "integration" ]; then
+ echo -e "${YELLOW}Running Integration Tests...${NC}"
+ phpunit --testsuite=integration
+elif [ "$1" = "coverage" ]; then
+ echo -e "${YELLOW}Running Tests with Code Coverage...${NC}"
+ phpunit --coverage-html coverage-report --coverage-text
+ echo -e "${GREEN}Coverage report generated in coverage-report/${NC}"
+elif [ "$1" = "specific" ] && [ -n "$2" ]; then
+ echo -e "${YELLOW}Running Specific Test: $2${NC}"
+ phpunit "$2"
+else
+ echo -e "${YELLOW}Running All Tests...${NC}"
+ phpunit
+fi
+
+# Check exit status
+if [ $? -eq 0 ]; then
+ echo -e "${GREEN}✓ Tests passed successfully!${NC}"
+else
+ echo -e "${RED}✗ Tests failed!${NC}"
+ exit 1
+fi
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php
index bc2afc64..6d23eed7 100644
--- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php
@@ -44,7 +44,7 @@ function hvac_ce_create_required_pages() {
],
'hvac-dashboard' => [
'title' => 'Trainer Dashboard',
- 'content' => '', // Content handled by template or redirect
+ 'content' => '[hvac_trainer_dashboard]',
],
'manage-event' => [ // New page for TEC CE submission form shortcode
'title' => 'Manage Event',
@@ -217,4 +217,5 @@ function hvac_ce_include_order_summary_template( $template ) {
}
return $template;
}
-add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 );
+// Removed - template handling is now in the main class
+// add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 );
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/admin/class-zoho-admin.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/admin/class-zoho-admin.php
new file mode 100644
index 00000000..6e3bfc7e
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/admin/class-zoho-admin.php
@@ -0,0 +1,231 @@
+ admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce('hvac_zoho_nonce')
+ ));
+
+ wp_enqueue_style(
+ 'hvac-zoho-admin',
+ HVAC_PLUGIN_URL . 'assets/css/zoho-admin.css',
+ array(),
+ HVAC_VERSION
+ );
+ }
+
+ /**
+ * Render admin page
+ */
+ public function render_admin_page() {
+ $config_file = HVAC_PLUGIN_DIR . 'includes/zoho/zoho-config.php';
+ $is_configured = file_exists($config_file);
+ $site_url = get_site_url();
+ $is_staging = strpos($site_url, 'upskillhvac.com') === false;
+ ?>
+
+
Zoho CRM Sync
+
+
+
+
🔧 STAGING MODE ACTIVE
+
Current site:
+
Staging mode is active. Data sync will be simulated only. No actual data will be sent to Zoho CRM.
+
Production sync is only enabled on upskillhvac.com
+
+
+
+
+
+
Zoho CRM is not configured. Please complete the OAuth setup first.
+
Run: ./bin/zoho-setup-complete.sh
+
+
+
+
Connection Status
+
Test Connection
+
+
+
+
+
Data Sync
+
+
+
Events → Campaigns
+
Sync events from The Events Calendar to Zoho CRM Campaigns
+
Sync Events
+
+
+
+
+
Users → Contacts
+
Sync trainers and attendees to Zoho CRM Contacts
+
Sync Users
+
+
+
+
+
Purchases → Invoices
+
Sync ticket purchases to Zoho CRM Invoices
+
Sync Purchases
+
+
+
+
+
+
Sync Settings
+
+
+
+
+ make_api_request('GET', '/crm/v2/settings/modules');
+
+ if ($response && !isset($response['error'])) {
+ wp_send_json_success(array(
+ 'message' => 'Connection successful!',
+ 'modules' => count($response['modules']) . ' modules available'
+ ));
+ } else {
+ wp_send_json_error(array(
+ 'message' => 'Connection failed',
+ 'error' => isset($response['error']) ? $response['error'] : 'Unknown error'
+ ));
+ }
+ } catch (Exception $e) {
+ wp_send_json_error(array(
+ 'message' => 'Connection failed',
+ 'error' => $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * Sync data to Zoho
+ */
+ public function sync_data() {
+ check_ajax_referer('hvac_zoho_nonce', 'nonce');
+
+ if (!current_user_can('manage_options')) {
+ wp_die('Unauthorized');
+ }
+
+ $type = sanitize_text_field($_POST['type']);
+
+ try {
+ require_once HVAC_PLUGIN_DIR . 'includes/zoho/class-zoho-sync.php';
+ $sync = new HVAC_Zoho_Sync();
+
+ switch ($type) {
+ case 'events':
+ $result = $sync->sync_events();
+ break;
+ case 'users':
+ $result = $sync->sync_users();
+ break;
+ case 'purchases':
+ $result = $sync->sync_purchases();
+ break;
+ default:
+ throw new Exception('Invalid sync type');
+ }
+
+ wp_send_json_success($result);
+ } catch (Exception $e) {
+ wp_send_json_error(array(
+ 'message' => 'Sync failed',
+ 'error' => $e->getMessage()
+ ));
+ }
+ }
+}
+
+// Initialize the admin interface
+new HVAC_Zoho_Admin();
+?>
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-author-fixer.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-author-fixer.php
new file mode 100644
index 00000000..2132bf3a
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-author-fixer.php
@@ -0,0 +1,96 @@
+ 0) {
+ $submission['post_author'] = $current_user_id;
+ HVAC_Logger::info('Setting event author to current user: ' . $current_user_id, 'EventAuthor');
+ }
+ }
+
+ return $submission;
+ }
+
+ /**
+ * Fix event author after creation
+ *
+ * @param int $event_id The event ID
+ */
+ public function fix_event_author($event_id) {
+ $event = get_post($event_id);
+
+ if ($event && $event->post_type === 'tribe_events') {
+ $current_user_id = get_current_user_id();
+
+ // If the event has no author or author is 0, set it to current user
+ if (($event->post_author == 0 || empty($event->post_author)) && $current_user_id > 0) {
+ wp_update_post(array(
+ 'ID' => $event_id,
+ 'post_author' => $current_user_id
+ ));
+
+ HVAC_Logger::info('Fixed event author for event ' . $event_id . ' to user ' . $current_user_id, 'EventAuthor');
+ }
+ }
+ }
+
+ /**
+ * Set post author when inserting tribe_events post
+ *
+ * @param array $data The post data
+ * @param array $postarr The post array
+ * @return array Modified post data
+ */
+ public function set_post_author($data, $postarr) {
+ // Only handle tribe_events posts
+ if ($data['post_type'] === 'tribe_events') {
+ $current_user_id = get_current_user_id();
+
+ // If no author is set and we have a logged-in user, set the author
+ if ((empty($data['post_author']) || $data['post_author'] == 0) && $current_user_id > 0) {
+ $data['post_author'] = $current_user_id;
+ HVAC_Logger::info('Setting tribe_events post author to: ' . $current_user_id, 'EventAuthor');
+ }
+ }
+
+ return $data;
+ }
+}
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-form-handler.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-form-handler.php
new file mode 100644
index 00000000..799ec479
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-event-form-handler.php
@@ -0,0 +1,55 @@
+remove_trainer_role();
-
- // Additional deactivation tasks
- // ...
+ HVAC_Roles::remove_hvac_trainer_role();
+ HVAC_Logger::info('Deactivation completed: HVAC trainer role removed.', 'Core');
}
/**
- * Initialize plugin actions attached to 'init' hook
+ * Initialize function (hooked on 'init')
*/
public function init() {
- HVAC_Logger::info('Init method started', 'Core');
- // Initialize handlers
- new \HVAC_Community_Events\Community\Login_Handler();
- HVAC_Logger::info('Login_Handler initialized', 'Core');
- new HVAC_Registration();
- HVAC_Logger::info('HVAC_Registration initialized', 'Core');
- HVAC_Logger::info('Init method completed', 'Core');
-
- // Prevent trainers from accessing wp-admin
- add_action('admin_init', array($this, 'redirect_trainers_from_admin'));
- }
-
- /**
- * Redirect HVAC trainers from admin area to frontend dashboard
- */
- public function redirect_trainers_from_admin() {
- if (defined('DOING_AJAX') && DOING_AJAX) {
- return;
- }
-
- // Check if user is trying to access wp-admin and has trainer role but not admin caps
- if ( is_admin() && ! current_user_can('manage_options') && current_user_can('view_hvac_dashboard') ) {
- wp_redirect(home_url('/hvac-dashboard/')); // Corrected slug
- exit;
+ // Initialize roles
+ $this->init_roles();
+
+ // Initialize forms
+ $this->init_forms();
+
+ // Initialize shortcodes
+ $this->init_shortcodes();
+
+ // Initialize event form handler
+ if (class_exists('HVAC_Community_Events\Event_Form_Handler')) {
+ new \HVAC_Community_Events\Event_Form_Handler();
}
}
/**
- * Load custom templates for plugin pages.
- *
- * @param string $template The path of the template to include.
- * @return string The path of the template to include.
+ * Initialize roles
*/
- public function load_custom_templates( $template ) {
- // Check if we are on the HVAC Dashboard page
- if ( is_page( 'hvac-dashboard' ) ) {
- $new_template = HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php';
- if ( file_exists( $new_template ) ) {
- return $new_template;
+ private function init_roles() {
+ $roles = new HVAC_Roles();
+ // Note: Role creation is handled in the activate method or the class constructor
+ }
+
+ /**
+ * Initialize forms
+ */
+ private function init_forms() {
+ $registration = new HVAC_Registration();
+ // Note: Form registration is handled in the class constructor
+ }
+
+ /**
+ * Initialize shortcodes
+ */
+ private function init_shortcodes() {
+ // Registration form shortcode
+ add_shortcode('hvac_trainer_registration', array('HVAC_Registration', 'render_registration_form'));
+
+ // Community login shortcode
+ add_shortcode('hvac_community_login', array('HVAC_Community_Login_Handler', 'render_login_form'));
+
+ // Dashboard shortcode
+ add_shortcode('hvac_dashboard', array($this, 'render_dashboard'));
+
+ // Add the event summary shortcode
+ add_shortcode('hvac_event_summary', array($this, 'render_event_summary'));
+
+ // Remove the event form shortcode as we're using TEC's shortcode instead
+ // add_shortcode('hvac_event_form', array('HVAC_Community_Event_Handler', 'render_event_form'));
+
+ // Add future shortcodes here
+ }
+
+ /**
+ * Render dashboard content
+ */
+ public function render_dashboard() {
+ if (!is_user_logged_in()) {
+ return 'Please log in to view the dashboard.
';
+ }
+
+ // Include the dashboard template
+ ob_start();
+ include HVAC_CE_PLUGIN_DIR . 'templates/dashboard/trainer-dashboard.php';
+ return ob_get_clean();
+ }
+
+ /**
+ * Render event summary content
+ */
+ public function render_event_summary() {
+ // This can be used to display custom event summary content
+ return 'Event Summary Content Here
';
+ }
+
+ /**
+ * Include custom templates for plugin pages
+ */
+ public function load_custom_templates($template) {
+ // Check for dashboard page
+ if (is_page('hvac-dashboard')) {
+ $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php';
+ if (file_exists($custom_template)) {
+ return $custom_template;
}
}
- // Add checks for other custom pages here if needed
+ // Check for my-events page
+ if (is_page('my-events')) {
+ $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/page-my-events.php';
+ if (file_exists($custom_template)) {
+ return $custom_template;
+ }
+ }
+
+ // Check for single event view (temporary)
+ if ( is_singular( 'tribe_events' ) ) {
+ $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/single-tribe_events.php';
+ if ( file_exists( $custom_template ) ) {
+ return $custom_template;
+ }
+ }
+
+ // Add future custom templates here
return $template;
- }
+ } // End load_custom_templates
+
} // End class HVAC_Community_Events
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-fixed.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-fixed.php
new file mode 100644
index 00000000..e5c3f496
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-fixed.php
@@ -0,0 +1,243 @@
+user_id = $user_id;
+ }
+
+ /**
+ * Get the total number of events created by the trainer.
+ *
+ * @return int
+ */
+ public function get_total_events_count() : int {
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ 'author' => $this->user_id,
+ 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ),
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ );
+ $query = new WP_Query( $args );
+ return (int) $query->found_posts;
+ }
+
+ /**
+ * Get the number of upcoming events for the trainer.
+ *
+ * @return int
+ */
+ public function get_upcoming_events_count() : int {
+ $today = current_time( 'mysql' );
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ 'author' => $this->user_id, // Use author consistently
+ 'post_status' => array( 'publish', 'future' ),
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ 'meta_query' => array(
+ array(
+ 'key' => '_EventStartDate',
+ 'value' => $today,
+ 'compare' => '>=',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ 'orderby' => 'meta_value',
+ 'meta_key' => '_EventStartDate',
+ 'order' => 'ASC',
+ );
+ $query = new WP_Query( $args );
+ return (int) $query->found_posts;
+ }
+
+ /**
+ * Get the number of past events for the trainer.
+ *
+ * @return int
+ */
+ public function get_past_events_count() : int {
+ $today = current_time( 'mysql' );
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ 'author' => $this->user_id, // Use author consistently
+ 'post_status' => array( 'publish', 'private' ),
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ 'meta_query' => array(
+ array(
+ 'key' => '_EventEndDate',
+ 'value' => $today,
+ 'compare' => '<',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ );
+ $query = new WP_Query( $args );
+ return (int) $query->found_posts;
+ }
+
+ /**
+ * Get the total number of tickets sold across all the trainer's events.
+ *
+ * @return int
+ */
+ public function get_total_tickets_sold() : int {
+ $total_tickets = 0;
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ 'author' => $this->user_id, // Use author consistently
+ 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ),
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ );
+ $event_ids = get_posts( $args );
+
+ if ( ! empty( $event_ids ) ) {
+ foreach ( $event_ids as $event_id ) {
+ $sold = get_post_meta( $event_id, '_tribe_tickets_sold', true );
+ if ( is_numeric( $sold ) ) {
+ $total_tickets += (int) $sold;
+ }
+ }
+ }
+
+ return $total_tickets;
+ }
+
+ /**
+ * Get the total revenue generated across all the trainer's events.
+ *
+ * @return float
+ */
+ public function get_total_revenue() : float {
+ $total_revenue = 0.0;
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ 'author' => $this->user_id, // Use author consistently
+ 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ),
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ );
+ $event_ids = get_posts( $args );
+
+ if ( ! empty( $event_ids ) ) {
+ foreach ( $event_ids as $event_id ) {
+ $revenue = get_post_meta( $event_id, '_tribe_revenue_total', true );
+ if ( is_numeric( $revenue ) ) {
+ $total_revenue += (float) $revenue;
+ }
+ }
+ }
+
+ return $total_revenue;
+ }
+
+ /**
+ * Get the annual revenue target set by the trainer.
+ *
+ * @return float|null Returns the target as a float, or null if not set.
+ */
+ public function get_annual_revenue_target() : ?float {
+ $target = get_user_meta( $this->user_id, 'annual_revenue_target', true );
+ return ! empty( $target ) && is_numeric( $target ) ? (float) $target : null;
+ }
+
+ /**
+ * Get the data needed for the events table on the dashboard.
+ *
+ * @param string $filter_status The status to filter events by.
+ * @return array An array of event data arrays.
+ */
+ public function get_events_table_data( string $filter_status = 'all' ) : array {
+ $events_data = [];
+ $valid_statuses = array( 'publish', 'future', 'draft', 'pending', 'private' );
+ $post_status = ( 'all' === $filter_status || ! in_array( $filter_status, $valid_statuses, true ) )
+ ? $valid_statuses
+ : array( $filter_status );
+
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ 'author' => $this->user_id, // Use author consistently
+ 'post_status' => $post_status,
+ 'posts_per_page' => -1,
+ 'orderby' => 'meta_value',
+ 'meta_key' => '_EventStartDate',
+ 'order' => 'DESC',
+ );
+
+ $query = new WP_Query( $args );
+
+ if ( $query->have_posts() ) {
+ while ( $query->have_posts() ) {
+ $query->the_post();
+ $event_id = get_the_ID();
+
+ // Get Capacity
+ $total_capacity = 0;
+ if ( function_exists( 'tribe_get_tickets' ) ) {
+ $tickets = tribe_get_tickets( $event_id );
+ if ( $tickets ) {
+ foreach ( $tickets as $ticket ) {
+ $capacity = $ticket->capacity();
+ if ( $capacity === -1 ) {
+ $total_capacity = -1;
+ break;
+ }
+ if ( is_numeric( $capacity ) ) {
+ $total_capacity += $capacity;
+ }
+ }
+ }
+ }
+
+ $sold = get_post_meta( $event_id, '_tribe_tickets_sold', true );
+ $revenue = get_post_meta( $event_id, '_tribe_revenue_total', true );
+
+ $events_data[] = array(
+ 'id' => $event_id,
+ 'status' => get_post_status( $event_id ),
+ 'name' => get_the_title(),
+ 'link' => get_permalink( $event_id ),
+ 'start_date_ts' => strtotime( get_post_meta( $event_id, '_EventStartDate', true ) ),
+ 'organizer_id' => (int) get_post_meta( $event_id, '_EventOrganizerID', true ),
+ 'capacity' => ( $total_capacity === -1 ) ? 'Unlimited' : (int) $total_capacity,
+ 'sold' => is_numeric( $sold ) ? (int) $sold : 0,
+ 'revenue' => is_numeric( $revenue ) ? (float) $revenue : 0.0,
+ );
+ }
+ wp_reset_postdata();
+ }
+
+ return $events_data;
+ }
+}
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-refactored.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-refactored.php
new file mode 100644
index 00000000..93af2ae9
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard-data-refactored.php
@@ -0,0 +1,335 @@
+user_id = $user_id;
+ }
+
+ /**
+ * Get all dashboard stats in a single cached object
+ *
+ * @return array
+ */
+ public function get_all_stats() : array {
+ $cache_key = 'stats_' . $this->user_id;
+ $stats = wp_cache_get( $cache_key, $this->cache_group );
+
+ if ( false === $stats ) {
+ $stats = array(
+ 'total_events' => $this->calculate_total_events_count(),
+ 'upcoming_events' => $this->calculate_upcoming_events_count(),
+ 'past_events' => $this->calculate_past_events_count(),
+ 'total_tickets' => $this->calculate_total_tickets_sold(),
+ 'total_revenue' => $this->calculate_total_revenue(),
+ 'revenue_target' => $this->get_annual_revenue_target(),
+ );
+
+ wp_cache_set( $cache_key, $stats, $this->cache_group, $this->cache_expiration );
+ HVAC_Logger::info( 'Dashboard stats calculated and cached', 'Dashboard', array( 'user_id' => $this->user_id ) );
+ }
+
+ return $stats;
+ }
+
+ /**
+ * Clear cache for a specific user
+ *
+ * @return void
+ */
+ public function clear_cache() {
+ $cache_key = 'stats_' . $this->user_id;
+ wp_cache_delete( $cache_key, $this->cache_group );
+ HVAC_Logger::info( 'Dashboard cache cleared', 'Dashboard', array( 'user_id' => $this->user_id ) );
+ }
+
+ /**
+ * Calculate total events count (optimized)
+ *
+ * @return int
+ */
+ private function calculate_total_events_count() : int {
+ global $wpdb;
+
+ // Direct query is more efficient for simple counts
+ $count = $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(ID) FROM {$wpdb->posts}
+ WHERE post_type = %s
+ AND post_author = %d
+ AND post_status IN ('publish', 'future', 'draft', 'pending', 'private')",
+ Tribe__Events__Main::POSTTYPE,
+ $this->user_id
+ ) );
+
+ return (int) $count;
+ }
+
+ /**
+ * Calculate upcoming events count
+ *
+ * @return int
+ */
+ private function calculate_upcoming_events_count() : int {
+ global $wpdb;
+
+ $today = current_time( 'mysql' );
+
+ // Query using post_author and meta data
+ $count = $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(DISTINCT p.ID)
+ FROM {$wpdb->posts} p
+ INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
+ WHERE p.post_type = %s
+ AND p.post_author = %d
+ AND p.post_status IN ('publish', 'future')
+ AND pm.meta_key = '_EventStartDate'
+ AND pm.meta_value >= %s",
+ Tribe__Events__Main::POSTTYPE,
+ $this->user_id,
+ $today
+ ) );
+
+ return (int) $count;
+ }
+
+ /**
+ * Calculate past events count
+ *
+ * @return int
+ */
+ private function calculate_past_events_count() : int {
+ global $wpdb;
+
+ $today = current_time( 'mysql' );
+
+ $count = $wpdb->get_var( $wpdb->prepare(
+ "SELECT COUNT(DISTINCT p.ID)
+ FROM {$wpdb->posts} p
+ INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
+ WHERE p.post_type = %s
+ AND p.post_author = %d
+ AND p.post_status IN ('publish', 'private')
+ AND pm.meta_key = '_EventEndDate'
+ AND pm.meta_value < %s",
+ Tribe__Events__Main::POSTTYPE,
+ $this->user_id,
+ $today
+ ) );
+
+ return (int) $count;
+ }
+
+ /**
+ * Calculate total tickets sold (optimized with single query)
+ *
+ * @return int
+ */
+ private function calculate_total_tickets_sold() : int {
+ global $wpdb;
+
+ // Get all event IDs in one query
+ $event_ids = $wpdb->get_col( $wpdb->prepare(
+ "SELECT ID FROM {$wpdb->posts}
+ WHERE post_type = %s
+ AND post_author = %d
+ AND post_status IN ('publish', 'future', 'draft', 'pending', 'private')",
+ Tribe__Events__Main::POSTTYPE,
+ $this->user_id
+ ) );
+
+ if ( empty( $event_ids ) ) {
+ return 0;
+ }
+
+ // Get sum of tickets sold in one query
+ $placeholders = array_fill( 0, count( $event_ids ), '%d' );
+ $sql = $wpdb->prepare(
+ "SELECT SUM(meta_value)
+ FROM {$wpdb->postmeta}
+ WHERE meta_key = '_tribe_tickets_sold'
+ AND post_id IN (" . implode( ',', $placeholders ) . ")",
+ $event_ids
+ );
+
+ $total = $wpdb->get_var( $sql );
+
+ return (int) $total;
+ }
+
+ /**
+ * Calculate total revenue (optimized)
+ *
+ * @return float
+ */
+ private function calculate_total_revenue() : float {
+ global $wpdb;
+
+ // Get all event IDs in one query
+ $event_ids = $wpdb->get_col( $wpdb->prepare(
+ "SELECT ID FROM {$wpdb->posts}
+ WHERE post_type = %s
+ AND post_author = %d
+ AND post_status IN ('publish', 'future', 'draft', 'pending', 'private')",
+ Tribe__Events__Main::POSTTYPE,
+ $this->user_id
+ ) );
+
+ if ( empty( $event_ids ) ) {
+ return 0.0;
+ }
+
+ // Get sum of revenue in one query
+ $placeholders = array_fill( 0, count( $event_ids ), '%d' );
+ $sql = $wpdb->prepare(
+ "SELECT SUM(meta_value)
+ FROM {$wpdb->postmeta}
+ WHERE meta_key = '_tribe_revenue_total'
+ AND post_id IN (" . implode( ',', $placeholders ) . ")",
+ $event_ids
+ );
+
+ $total = $wpdb->get_var( $sql );
+
+ return (float) $total;
+ }
+
+ /**
+ * Get annual revenue target
+ *
+ * @return float|null
+ */
+ private function get_annual_revenue_target() : ?float {
+ $target = get_user_meta( $this->user_id, 'annual_revenue_target', true );
+ return ! empty( $target ) && is_numeric( $target ) ? (float) $target : null;
+ }
+
+ /**
+ * Get events table data (optimized)
+ *
+ * @param string $filter_status Status filter
+ * @return array
+ */
+ public function get_events_table_data( string $filter_status = 'all' ) : array {
+ global $wpdb;
+
+ $valid_statuses = array( 'publish', 'future', 'draft', 'pending', 'private' );
+ $post_status = ( 'all' === $filter_status || ! in_array( $filter_status, $valid_statuses, true ) )
+ ? $valid_statuses
+ : array( $filter_status );
+
+ // Convert to SQL-safe string
+ $status_placeholders = array_fill( 0, count( $post_status ), '%s' );
+ $status_sql = implode( ',', $status_placeholders );
+
+ // Get all events with their metadata in fewer queries
+ $sql = $wpdb->prepare(
+ "SELECT p.ID, p.post_title, p.post_status, p.guid,
+ MAX(CASE WHEN pm.meta_key = '_EventStartDate' THEN pm.meta_value END) as start_date,
+ MAX(CASE WHEN pm.meta_key = '_EventOrganizerID' THEN pm.meta_value END) as organizer_id,
+ MAX(CASE WHEN pm.meta_key = '_tribe_tickets_sold' THEN pm.meta_value END) as tickets_sold,
+ MAX(CASE WHEN pm.meta_key = '_tribe_revenue_total' THEN pm.meta_value END) as revenue
+ FROM {$wpdb->posts} p
+ LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
+ WHERE p.post_type = %s
+ AND p.post_author = %d
+ AND p.post_status IN ($status_sql)
+ GROUP BY p.ID
+ ORDER BY start_date DESC",
+ array_merge(
+ array( Tribe__Events__Main::POSTTYPE, $this->user_id ),
+ $post_status
+ )
+ );
+
+ $events = $wpdb->get_results( $sql );
+ $events_data = array();
+
+ foreach ( $events as $event ) {
+ // Get ticket capacity
+ $capacity = $this->get_event_capacity( $event->ID );
+
+ $events_data[] = array(
+ 'id' => $event->ID,
+ 'status' => $event->post_status,
+ 'name' => $event->post_title,
+ 'link' => get_permalink( $event->ID ),
+ 'start_date_ts' => strtotime( $event->start_date ),
+ 'organizer_id' => (int) $event->organizer_id,
+ 'capacity' => $capacity,
+ 'sold' => (int) $event->tickets_sold,
+ 'revenue' => (float) $event->revenue,
+ );
+ }
+
+ return $events_data;
+ }
+
+ /**
+ * Get event capacity
+ *
+ * @param int $event_id Event ID
+ * @return string|int
+ */
+ private function get_event_capacity( $event_id ) {
+ if ( ! function_exists( 'tribe_get_tickets' ) ) {
+ return 0;
+ }
+
+ $tickets = tribe_get_tickets( $event_id );
+ $total_capacity = 0;
+
+ foreach ( $tickets as $ticket ) {
+ $capacity = $ticket->capacity();
+ if ( $capacity === -1 ) {
+ return 'Unlimited';
+ }
+ if ( is_numeric( $capacity ) ) {
+ $total_capacity += $capacity;
+ }
+ }
+
+ return $total_capacity;
+ }
+}
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard.php
new file mode 100644
index 00000000..787a8b21
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-dashboard.php
@@ -0,0 +1,394 @@
+
+ Please log in to view the dashboard.
+ Login
+ ';
+ }
+
+ if (!current_user_can('view_hvac_dashboard')) {
+ return '
+
You do not have permission to view this dashboard.
+
';
+ }
+
+ return $this->get_dashboard_content();
+ }
+
+ /**
+ * Render dashboard content for the page
+ */
+ public function render_dashboard_content($content) {
+ // Only process if content contains our shortcode
+ if (has_shortcode($content, 'hvac_trainer_dashboard')) {
+ return do_shortcode($content);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Get dashboard content
+ */
+ private function get_dashboard_content() {
+ $user_id = get_current_user_id();
+
+ // Include dashboard data class
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-dashboard-data.php';
+ $dashboard_data = new HVAC_Dashboard_Data($user_id);
+
+ // Get data
+ $data = array(
+ 'total_events' => $dashboard_data->get_total_events_count(),
+ 'upcoming_events' => $dashboard_data->get_upcoming_events_count(),
+ 'past_events' => $dashboard_data->get_past_events_count(),
+ 'total_sold' => $dashboard_data->get_total_tickets_sold(),
+ 'total_revenue' => $dashboard_data->get_total_revenue(),
+ 'revenue_target' => $dashboard_data->get_annual_revenue_target(),
+ 'events_table' => $dashboard_data->get_events_table_data(isset($_GET['event_status']) ? sanitize_key($_GET['event_status']) : 'all'),
+ 'current_filter' => isset($_GET['event_status']) ? sanitize_key($_GET['event_status']) : 'all'
+ );
+
+ // Get dashboard HTML
+ ob_start();
+ ?>
+
+
+
+
+
+
+ Your Stats
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Total Revenue
+
$
+
+
Target: $
+
+
+
+
+
+
+
+ Your Events
+
+
+
+
+
+
+
+
+
+
+ Status
+ Event Name
+ Date
+ Organizer
+ Capacity
+ Sold
+ Revenue
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $
+
+
+ Edit |
+ Summary
+
+
+
+
+
+
+
No events found.
+
+
+
+
+ nonce_action = $nonce_action;
+ $this->set_default_attributes();
+ }
+
+ /**
+ * Set default form attributes
+ */
+ private function set_default_attributes() {
+ $this->form_attrs = array(
+ 'method' => 'post',
+ 'action' => '',
+ 'id' => '',
+ 'class' => 'hvac-form',
+ 'enctype' => 'application/x-www-form-urlencoded',
+ );
+ }
+
+ /**
+ * Set form attributes
+ *
+ * @param array $attrs Form attributes
+ * @return self
+ */
+ public function set_attributes( $attrs ) {
+ $this->form_attrs = array_merge( $this->form_attrs, $attrs );
+ return $this;
+ }
+
+ /**
+ * Add a field to the form
+ *
+ * @param array $field Field configuration
+ * @return self
+ */
+ public function add_field( $field ) {
+ $defaults = array(
+ 'type' => 'text',
+ 'name' => '',
+ 'label' => '',
+ 'value' => '',
+ 'required' => false,
+ 'placeholder' => '',
+ 'class' => '',
+ 'id' => '',
+ 'options' => array(),
+ 'sanitize' => 'text',
+ 'validate' => array(),
+ 'description' => '',
+ 'wrapper_class' => 'form-row',
+ );
+
+ $field = wp_parse_args( $field, $defaults );
+
+ // Auto-generate ID if not provided
+ if ( empty( $field['id'] ) && ! empty( $field['name'] ) ) {
+ $field['id'] = sanitize_html_class( $field['name'] );
+ }
+
+ $this->fields[] = $field;
+ return $this;
+ }
+
+ /**
+ * Set form data
+ *
+ * @param array $data Form data
+ * @return self
+ */
+ public function set_data( $data ) {
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Set form errors
+ *
+ * @param array $errors Form errors
+ * @return self
+ */
+ public function set_errors( $errors ) {
+ $this->errors = $errors;
+ return $this;
+ }
+
+ /**
+ * Render the form
+ *
+ * @return string
+ */
+ public function render() {
+ ob_start();
+ ?>
+
+ form_attrs as $key => $value ) {
+ if ( ! empty( $value ) ) {
+ $attrs[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) );
+ }
+ }
+ return implode( ' ', $attrs );
+ }
+
+ /**
+ * Render a single field
+ *
+ * @param array $field Field configuration
+ * @return string
+ */
+ private function render_field( $field ) {
+ $output = sprintf( '', esc_attr( $field['wrapper_class'] ) );
+
+ // Label
+ if ( ! empty( $field['label'] ) ) {
+ $output .= sprintf(
+ '%s%s ',
+ esc_attr( $field['id'] ),
+ esc_html( $field['label'] ),
+ $field['required'] ? ' * ' : ''
+ );
+ }
+
+ // Field
+ switch ( $field['type'] ) {
+ case 'select':
+ $output .= $this->render_select( $field );
+ break;
+ case 'textarea':
+ $output .= $this->render_textarea( $field );
+ break;
+ case 'checkbox':
+ $output .= $this->render_checkbox( $field );
+ break;
+ case 'radio':
+ $output .= $this->render_radio( $field );
+ break;
+ case 'file':
+ $output .= $this->render_file( $field );
+ break;
+ default:
+ $output .= $this->render_input( $field );
+ }
+
+ // Description
+ if ( ! empty( $field['description'] ) ) {
+ $output .= sprintf( '%s ', esc_html( $field['description'] ) );
+ }
+
+ // Error
+ if ( isset( $this->errors[ $field['name'] ] ) ) {
+ $output .= sprintf(
+ '%s ',
+ esc_html( $this->errors[ $field['name'] ] )
+ );
+ }
+
+ $output .= '
';
+ return $output;
+ }
+
+ /**
+ * Render input field
+ *
+ * @param array $field Field configuration
+ * @return string
+ */
+ private function render_input( $field ) {
+ $value = $this->get_field_value( $field['name'], $field['value'] );
+
+ return sprintf(
+ ' ',
+ esc_attr( $field['type'] ),
+ esc_attr( $field['name'] ),
+ esc_attr( $field['id'] ),
+ esc_attr( $value ),
+ esc_attr( $field['class'] ),
+ $field['required'] ? 'required' : '',
+ ! empty( $field['placeholder'] ) ? 'placeholder="' . esc_attr( $field['placeholder'] ) . '"' : ''
+ );
+ }
+
+ /**
+ * Render select field
+ *
+ * @param array $field Field configuration
+ * @return string
+ */
+ private function render_select( $field ) {
+ $value = $this->get_field_value( $field['name'], $field['value'] );
+
+ $output = sprintf(
+ '',
+ esc_attr( $field['name'] ),
+ esc_attr( $field['id'] ),
+ esc_attr( $field['class'] ),
+ $field['required'] ? 'required' : ''
+ );
+
+ foreach ( $field['options'] as $option_value => $option_label ) {
+ $output .= sprintf(
+ '%s ',
+ esc_attr( $option_value ),
+ selected( $value, $option_value, false ),
+ esc_html( $option_label )
+ );
+ }
+
+ $output .= ' ';
+ return $output;
+ }
+
+ /**
+ * Render textarea field
+ *
+ * @param array $field Field configuration
+ * @return string
+ */
+ private function render_textarea( $field ) {
+ $value = $this->get_field_value( $field['name'], $field['value'] );
+
+ return sprintf(
+ '',
+ esc_attr( $field['name'] ),
+ esc_attr( $field['id'] ),
+ esc_attr( $field['class'] ),
+ $field['required'] ? 'required' : '',
+ ! empty( $field['placeholder'] ) ? 'placeholder="' . esc_attr( $field['placeholder'] ) . '"' : '',
+ esc_textarea( $value )
+ );
+ }
+
+ /**
+ * Render checkbox field
+ *
+ * @param array $field Field configuration
+ * @return string
+ */
+ private function render_checkbox( $field ) {
+ $value = $this->get_field_value( $field['name'], $field['value'] );
+ $is_checked = ! empty( $value );
+
+ return sprintf(
+ ' ',
+ esc_attr( $field['name'] ),
+ esc_attr( $field['id'] ),
+ esc_attr( $field['class'] ),
+ checked( $is_checked, true, false )
+ );
+ }
+
+ /**
+ * Render radio field group
+ *
+ * @param array $field Field configuration
+ * @return string
+ */
+ private function render_radio( $field ) {
+ $value = $this->get_field_value( $field['name'], $field['value'] );
+ $output = '';
+
+ foreach ( $field['options'] as $option_value => $option_label ) {
+ $output .= sprintf(
+ ' %s ',
+ esc_attr( $field['name'] ),
+ esc_attr( $option_value ),
+ checked( $value, $option_value, false ),
+ esc_html( $option_label )
+ );
+ }
+
+ $output .= '
';
+ return $output;
+ }
+
+ /**
+ * Render file field
+ *
+ * @param array $field Field configuration
+ * @return string
+ */
+ private function render_file( $field ) {
+ // Ensure form has proper enctype
+ $this->form_attrs['enctype'] = 'multipart/form-data';
+
+ return sprintf(
+ ' ',
+ esc_attr( $field['name'] ),
+ esc_attr( $field['id'] ),
+ esc_attr( $field['class'] ),
+ $field['required'] ? 'required' : ''
+ );
+ }
+
+ /**
+ * Get field value from data or default
+ *
+ * @param string $name Field name
+ * @param mixed $default Default value
+ * @return mixed
+ */
+ private function get_field_value( $name, $default = '' ) {
+ return isset( $this->data[ $name ] ) ? $this->data[ $name ] : $default;
+ }
+
+ /**
+ * Validate form data
+ *
+ * @param array $data Form data to validate
+ * @return array Validation errors
+ */
+ public function validate( $data ) {
+ $errors = array();
+
+ foreach ( $this->fields as $field ) {
+ $value = isset( $data[ $field['name'] ] ) ? $data[ $field['name'] ] : '';
+
+ // Required field check
+ if ( $field['required'] && empty( $value ) ) {
+ $errors[ $field['name'] ] = sprintf( '%s is required.', $field['label'] );
+ continue;
+ }
+
+ // Custom validation rules
+ if ( ! empty( $field['validate'] ) && ! empty( $value ) ) {
+ foreach ( $field['validate'] as $rule => $params ) {
+ $error = $this->apply_validation_rule( $value, $rule, $params, $field );
+ if ( $error ) {
+ $errors[ $field['name'] ] = $error;
+ break;
+ }
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Apply validation rule
+ *
+ * @param mixed $value Value to validate
+ * @param string $rule Validation rule
+ * @param mixed $params Rule parameters
+ * @param array $field Field configuration
+ * @return string|false Error message or false if valid
+ */
+ private function apply_validation_rule( $value, $rule, $params, $field ) {
+ switch ( $rule ) {
+ case 'email':
+ if ( ! is_email( $value ) ) {
+ return sprintf( '%s must be a valid email address.', $field['label'] );
+ }
+ break;
+ case 'url':
+ if ( ! filter_var( $value, FILTER_VALIDATE_URL ) ) {
+ return sprintf( '%s must be a valid URL.', $field['label'] );
+ }
+ break;
+ case 'min_length':
+ if ( strlen( $value ) < $params ) {
+ return sprintf( '%s must be at least %d characters long.', $field['label'], $params );
+ }
+ break;
+ case 'max_length':
+ if ( strlen( $value ) > $params ) {
+ return sprintf( '%s must not exceed %d characters.', $field['label'], $params );
+ }
+ break;
+ case 'pattern':
+ if ( ! preg_match( $params, $value ) ) {
+ return sprintf( '%s has an invalid format.', $field['label'] );
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ /**
+ * Sanitize form data
+ *
+ * @param array $data Raw form data
+ * @return array Sanitized data
+ */
+ public function sanitize( $data ) {
+ $sanitized = array();
+
+ foreach ( $this->fields as $field ) {
+ if ( ! isset( $data[ $field['name'] ] ) ) {
+ continue;
+ }
+
+ $value = $data[ $field['name'] ];
+
+ switch ( $field['sanitize'] ) {
+ case 'email':
+ $sanitized[ $field['name'] ] = sanitize_email( $value );
+ break;
+ case 'url':
+ $sanitized[ $field['name'] ] = esc_url_raw( $value );
+ break;
+ case 'textarea':
+ $sanitized[ $field['name'] ] = sanitize_textarea_field( $value );
+ break;
+ case 'int':
+ $sanitized[ $field['name'] ] = intval( $value );
+ break;
+ case 'float':
+ $sanitized[ $field['name'] ] = floatval( $value );
+ break;
+ case 'none':
+ $sanitized[ $field['name'] ] = $value;
+ break;
+ default:
+ $sanitized[ $field['name'] ] = sanitize_text_field( $value );
+ }
+ }
+
+ return $sanitized;
+ }
+}
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-logger.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-logger.php
new file mode 100644
index 00000000..fa5bcd68
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-logger.php
@@ -0,0 +1,156 @@
+ $action,
+ 'user_id' => get_current_user_id(),
+ ) );
+
+ if ( $die_on_fail ) {
+ wp_die( __( 'Security check failed. Please refresh the page and try again.', 'hvac-community-events' ) );
+ }
+ }
+
+ return $is_valid;
+ }
+
+ /**
+ * Check if current user has required capability
+ *
+ * @param string $capability The capability to check
+ * @param bool $die_on_fail Whether to die on failure
+ * @return bool
+ */
+ public static function check_capability( $capability, $die_on_fail = false ) {
+ $has_cap = current_user_can( $capability );
+
+ if ( ! $has_cap ) {
+ HVAC_Logger::warning( 'Capability check failed', 'Security', array(
+ 'capability' => $capability,
+ 'user_id' => get_current_user_id(),
+ ) );
+
+ if ( $die_on_fail ) {
+ wp_die( __( 'You do not have permission to perform this action.', 'hvac-community-events' ) );
+ }
+ }
+
+ return $has_cap;
+ }
+
+ /**
+ * Check if user is logged in with optional redirect
+ *
+ * @param string $redirect_to URL to redirect if not logged in
+ * @return bool
+ */
+ public static function require_login( $redirect_to = '' ) {
+ if ( ! is_user_logged_in() ) {
+ if ( ! empty( $redirect_to ) ) {
+ wp_safe_redirect( $redirect_to );
+ exit;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Sanitize and validate email
+ *
+ * @param string $email Email to validate
+ * @return string|false Sanitized email or false if invalid
+ */
+ public static function sanitize_email( $email ) {
+ $email = sanitize_email( $email );
+ return is_email( $email ) ? $email : false;
+ }
+
+ /**
+ * Sanitize and validate URL
+ *
+ * @param string $url URL to validate
+ * @return string|false Sanitized URL or false if invalid
+ */
+ public static function sanitize_url( $url ) {
+ $url = esc_url_raw( $url );
+ return filter_var( $url, FILTER_VALIDATE_URL ) ? $url : false;
+ }
+
+ /**
+ * Sanitize array of values
+ *
+ * @param array $array Array to sanitize
+ * @param string $type Type of sanitization (text|email|url|int)
+ * @return array
+ */
+ public static function sanitize_array( $array, $type = 'text' ) {
+ if ( ! is_array( $array ) ) {
+ return array();
+ }
+
+ $sanitized = array();
+ foreach ( $array as $key => $value ) {
+ switch ( $type ) {
+ case 'email':
+ $clean = self::sanitize_email( $value );
+ if ( $clean ) {
+ $sanitized[ $key ] = $clean;
+ }
+ break;
+ case 'url':
+ $clean = self::sanitize_url( $value );
+ if ( $clean ) {
+ $sanitized[ $key ] = $clean;
+ }
+ break;
+ case 'int':
+ $sanitized[ $key ] = intval( $value );
+ break;
+ default:
+ $sanitized[ $key ] = sanitize_text_field( $value );
+ }
+ }
+
+ return $sanitized;
+ }
+
+ /**
+ * Escape output based on context
+ *
+ * @param mixed $value Value to escape
+ * @param string $context Context (html|attr|url|js)
+ * @return string
+ */
+ public static function escape_output( $value, $context = 'html' ) {
+ switch ( $context ) {
+ case 'attr':
+ return esc_attr( $value );
+ case 'url':
+ return esc_url( $value );
+ case 'js':
+ return esc_js( $value );
+ case 'textarea':
+ return esc_textarea( $value );
+ default:
+ return esc_html( $value );
+ }
+ }
+
+ /**
+ * Check if request is AJAX
+ *
+ * @return bool
+ */
+ public static function is_ajax_request() {
+ return defined( 'DOING_AJAX' ) && DOING_AJAX;
+ }
+
+ /**
+ * Get user IP address
+ *
+ * @return string
+ */
+ public static function get_user_ip() {
+ $ip = '';
+
+ if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
+ $ip = $_SERVER['HTTP_CLIENT_IP'];
+ } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
+ $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+ } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
+ $ip = $_SERVER['REMOTE_ADDR'];
+ }
+
+ return sanitize_text_field( $ip );
+ }
+
+ /**
+ * Rate limiting check
+ *
+ * @param string $action Action to limit
+ * @param int $limit Number of attempts allowed
+ * @param int $window Time window in seconds
+ * @param string $identifier User identifier (defaults to IP)
+ * @return bool True if within limits, false if exceeded
+ */
+ public static function check_rate_limit( $action, $limit = 5, $window = 300, $identifier = null ) {
+ if ( null === $identifier ) {
+ $identifier = self::get_user_ip();
+ }
+
+ $key = 'hvac_rate_limit_' . md5( $action . $identifier );
+ $attempts = get_transient( $key );
+
+ if ( false === $attempts ) {
+ set_transient( $key, 1, $window );
+ return true;
+ }
+
+ if ( $attempts >= $limit ) {
+ HVAC_Logger::warning( 'Rate limit exceeded', 'Security', array(
+ 'action' => $action,
+ 'identifier' => $identifier,
+ 'attempts' => $attempts,
+ ) );
+ return false;
+ }
+
+ set_transient( $key, $attempts + 1, $window );
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-settings-refactored.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-settings-refactored.php
new file mode 100644
index 00000000..5e0526fb
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-settings-refactored.php
@@ -0,0 +1,411 @@
+set_defaults();
+ add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
+ add_action( 'admin_init', array( $this, 'register_settings' ) );
+ }
+
+ /**
+ * Set default settings
+ */
+ private function set_defaults() {
+ $this->defaults = array(
+ 'general' => array(
+ 'debug_mode' => false,
+ 'cache_duration' => 300,
+ 'enable_notifications' => true,
+ ),
+ 'registration' => array(
+ 'auto_approve' => false,
+ 'require_venue' => false,
+ 'email_verification' => true,
+ 'terms_url' => '',
+ ),
+ 'dashboard' => array(
+ 'items_per_page' => 20,
+ 'show_revenue' => true,
+ 'show_capacity' => true,
+ 'date_format' => 'Y-m-d',
+ ),
+ 'notifications' => array(
+ 'admin_email' => get_option( 'admin_email' ),
+ 'from_email' => get_option( 'admin_email' ),
+ 'from_name' => get_option( 'blogname' ),
+ ),
+ 'advanced' => array(
+ 'uninstall_data' => false,
+ 'legacy_support' => false,
+ ),
+ );
+ }
+
+ /**
+ * Get all settings
+ *
+ * @return array
+ */
+ public function get_settings() {
+ if ( null === $this->settings ) {
+ $this->settings = get_option( $this->option_name, array() );
+ $this->settings = wp_parse_args( $this->settings, $this->defaults );
+ }
+ return $this->settings;
+ }
+
+ /**
+ * Get a specific setting
+ *
+ * @param string $section Setting section
+ * @param string $key Setting key
+ * @param mixed $default Default value if not set
+ * @return mixed
+ */
+ public function get( $section, $key, $default = null ) {
+ $settings = $this->get_settings();
+
+ if ( isset( $settings[ $section ][ $key ] ) ) {
+ return $settings[ $section ][ $key ];
+ }
+
+ if ( null !== $default ) {
+ return $default;
+ }
+
+ return isset( $this->defaults[ $section ][ $key ] )
+ ? $this->defaults[ $section ][ $key ]
+ : null;
+ }
+
+ /**
+ * Update a setting
+ *
+ * @param string $section Setting section
+ * @param string $key Setting key
+ * @param mixed $value New value
+ * @return bool
+ */
+ public function update( $section, $key, $value ) {
+ $settings = $this->get_settings();
+
+ if ( ! isset( $settings[ $section ] ) ) {
+ $settings[ $section ] = array();
+ }
+
+ $settings[ $section ][ $key ] = $value;
+ $this->settings = $settings;
+
+ return update_option( $this->option_name, $settings );
+ }
+
+ /**
+ * Add settings page to admin menu
+ */
+ public function add_settings_page() {
+ add_options_page(
+ __( 'HVAC Community Events Settings', 'hvac-community-events' ),
+ __( 'HVAC Events', 'hvac-community-events' ),
+ 'manage_options',
+ $this->page_slug,
+ array( $this, 'render_settings_page' )
+ );
+ }
+
+ /**
+ * Register settings
+ */
+ public function register_settings() {
+ register_setting(
+ $this->settings_group,
+ $this->option_name,
+ array( $this, 'sanitize_settings' )
+ );
+
+ // General Settings Section
+ add_settings_section(
+ 'hvac_ce_general',
+ __( 'General Settings', 'hvac-community-events' ),
+ array( $this, 'render_section_general' ),
+ $this->page_slug
+ );
+
+ add_settings_field(
+ 'debug_mode',
+ __( 'Debug Mode', 'hvac-community-events' ),
+ array( $this, 'render_field_checkbox' ),
+ $this->page_slug,
+ 'hvac_ce_general',
+ array(
+ 'label_for' => 'debug_mode',
+ 'section' => 'general',
+ 'key' => 'debug_mode',
+ 'description' => __( 'Enable debug logging', 'hvac-community-events' ),
+ )
+ );
+
+ add_settings_field(
+ 'cache_duration',
+ __( 'Cache Duration', 'hvac-community-events' ),
+ array( $this, 'render_field_number' ),
+ $this->page_slug,
+ 'hvac_ce_general',
+ array(
+ 'label_for' => 'cache_duration',
+ 'section' => 'general',
+ 'key' => 'cache_duration',
+ 'description' => __( 'Cache duration in seconds', 'hvac-community-events' ),
+ 'min' => 60,
+ 'max' => 3600,
+ )
+ );
+
+ // Registration Settings Section
+ add_settings_section(
+ 'hvac_ce_registration',
+ __( 'Registration Settings', 'hvac-community-events' ),
+ array( $this, 'render_section_registration' ),
+ $this->page_slug
+ );
+
+ add_settings_field(
+ 'auto_approve',
+ __( 'Auto Approve', 'hvac-community-events' ),
+ array( $this, 'render_field_checkbox' ),
+ $this->page_slug,
+ 'hvac_ce_registration',
+ array(
+ 'label_for' => 'auto_approve',
+ 'section' => 'registration',
+ 'key' => 'auto_approve',
+ 'description' => __( 'Automatically approve new trainer registrations', 'hvac-community-events' ),
+ )
+ );
+
+ // Add more sections and fields as needed
+ }
+
+ /**
+ * Render settings page
+ */
+ public function render_settings_page() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ // Show success message if settings were saved
+ if ( isset( $_GET['settings-updated'] ) ) {
+ add_settings_error(
+ 'hvac_ce_settings',
+ 'hvac_ce_settings_message',
+ __( 'Settings saved.', 'hvac-community-events' ),
+ 'updated'
+ );
+ }
+
+ settings_errors( 'hvac_ce_settings' );
+ ?>
+
+
+
+
+ ' . __( 'Configure general plugin settings.', 'hvac-community-events' ) . '';
+ }
+
+ /**
+ * Render registration section description
+ */
+ public function render_section_registration() {
+ echo '' . __( 'Configure trainer registration settings.', 'hvac-community-events' ) . '
';
+ }
+
+ /**
+ * Render checkbox field
+ *
+ * @param array $args Field arguments
+ */
+ public function render_field_checkbox( $args ) {
+ $value = $this->get( $args['section'], $args['key'] );
+ ?>
+
+ />
+
+
+ get( $args['section'], $args['key'] );
+ ?>
+
+
+
+ get( $args['section'], $args['key'] );
+ ?>
+
+
+
+ ! empty( $input['general']['debug_mode'] ),
+ 'cache_duration' => absint( $input['general']['cache_duration'] ?? 300 ),
+ 'enable_notifications' => ! empty( $input['general']['enable_notifications'] ),
+ );
+
+ // Update debug mode in logger
+ HVAC_Logger::set_enabled( $sanitized['general']['debug_mode'] );
+ }
+
+ // Registration settings
+ if ( isset( $input['registration'] ) ) {
+ $sanitized['registration'] = array(
+ 'auto_approve' => ! empty( $input['registration']['auto_approve'] ),
+ 'require_venue' => ! empty( $input['registration']['require_venue'] ),
+ 'email_verification' => ! empty( $input['registration']['email_verification'] ),
+ 'terms_url' => esc_url_raw( $input['registration']['terms_url'] ?? '' ),
+ );
+ }
+
+ // Dashboard settings
+ if ( isset( $input['dashboard'] ) ) {
+ $sanitized['dashboard'] = array(
+ 'items_per_page' => absint( $input['dashboard']['items_per_page'] ?? 20 ),
+ 'show_revenue' => ! empty( $input['dashboard']['show_revenue'] ),
+ 'show_capacity' => ! empty( $input['dashboard']['show_capacity'] ),
+ 'date_format' => sanitize_text_field( $input['dashboard']['date_format'] ?? 'Y-m-d' ),
+ );
+ }
+
+ // Merge with existing settings to preserve sections not being updated
+ $existing = $this->get_settings();
+ $sanitized = wp_parse_args( $sanitized, $existing );
+
+ return $sanitized;
+ }
+
+ /**
+ * Get instance of settings class (singleton)
+ *
+ * @return self
+ */
+ public static function get_instance() {
+ static $instance = null;
+
+ if ( null === $instance ) {
+ $instance = new self();
+ }
+
+ return $instance;
+ }
+}
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/README.md b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/README.md
new file mode 100644
index 00000000..68fbdc98
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/README.md
@@ -0,0 +1,134 @@
+# Zoho CRM Integration Setup Guide
+
+## Overview
+This integration syncs WordPress Events Calendar data with Zoho CRM, mapping:
+- Events → Campaigns
+- Users/Trainers → Contacts
+- Ticket Purchases → Invoices
+
+## Setup Steps
+
+### 1. Create Zoho OAuth Application
+
+1. Go to [Zoho API Console](https://api-console.zoho.com/)
+2. Click "CREATE NEW CLIENT"
+3. Choose "Server-based Applications"
+4. Fill in details:
+ - **Client Name**: HVAC Community Events Integration
+ - **Homepage URL**: Your WordPress site URL
+ - **Authorized Redirect URIs**:
+ - For setup script: `http://localhost:8080/callback`
+ - For WordPress admin: `https://your-site.com/wp-admin/edit.php?post_type=tribe_events&page=hvac-zoho-crm`
+5. Click "CREATE" and save your Client ID and Client Secret
+
+### 2. Generate Credentials Using Setup Script
+
+The easiest way to set up credentials:
+
+```bash
+cd wp-content/plugins/hvac-community-events/includes/zoho
+php setup-helper.php
+```
+
+Follow the prompts to:
+1. Enter your Client ID and Client Secret
+2. Open the authorization URL in your browser
+3. Grant permissions and copy the authorization code
+4. The script will automatically:
+ - Exchange the code for tokens
+ - Get your organization ID
+ - Create the `zoho-config.php` file
+
+### 3. Alternative: Manual Setup
+
+If you prefer manual setup:
+
+1. Create `zoho-config.php` from the template:
+ ```bash
+ cp zoho-config-template.php zoho-config.php
+ ```
+
+2. Generate authorization URL:
+ ```
+ https://accounts.zoho.com/oauth/v2/auth?
+ scope=ZohoCRM.settings.all,ZohoCRM.modules.all,ZohoCRM.users.all&
+ client_id=YOUR_CLIENT_ID&
+ response_type=code&
+ access_type=offline&
+ redirect_uri=http://localhost:8080/callback
+ ```
+
+3. Exchange code for tokens using cURL:
+ ```bash
+ curl -X POST https://accounts.zoho.com/oauth/v2/token \
+ -d "grant_type=authorization_code" \
+ -d "client_id=YOUR_CLIENT_ID" \
+ -d "client_secret=YOUR_CLIENT_SECRET" \
+ -d "redirect_uri=http://localhost:8080/callback" \
+ -d "code=YOUR_AUTH_CODE"
+ ```
+
+4. Get organization ID:
+ ```bash
+ curl -X GET https://www.zohoapis.com/crm/v2/org \
+ -H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN"
+ ```
+
+5. Update `zoho-config.php` with your credentials
+
+### 4. WordPress Admin Setup
+
+After creating the config file:
+
+1. Go to WordPress Admin → Events → Zoho CRM
+2. The integration will automatically detect your configuration
+3. Click "Test Connection" to verify
+4. Click "Create Custom Fields" to set up required fields in Zoho
+
+## Required Permissions
+
+The integration needs these Zoho CRM scopes:
+- `ZohoCRM.settings.all` - For creating custom fields
+- `ZohoCRM.modules.all` - For reading/writing records
+- `ZohoCRM.users.all` - For user information
+- `ZohoCRM.org.all` - For organization details (optional)
+
+## Security Notes
+
+- **NEVER** commit `zoho-config.php` to version control
+- Keep your refresh token secure
+- The integration automatically handles token refresh
+- All API calls are logged for debugging (disable in production)
+
+## Troubleshooting
+
+### Common Issues
+
+1. **"Invalid Client" Error**
+ - Verify Client ID and Secret are correct
+ - Ensure redirect URI matches exactly
+
+2. **"Invalid Code" Error**
+ - Authorization codes expire quickly (< 1 minute)
+ - Generate and use immediately
+
+3. **"No Refresh Token" Error**
+ - Make sure `access_type=offline` in auth URL
+ - Include `prompt=consent` to force new refresh token
+
+### Debug Mode
+
+Enable debug logging in `zoho-config.php`:
+```php
+define('ZOHO_DEBUG_MODE', true);
+define('ZOHO_LOG_FILE', WP_CONTENT_DIR . '/zoho-crm-debug.log');
+```
+
+Check the log file for detailed API responses.
+
+## Support
+
+For issues or questions:
+1. Check the debug log
+2. Verify credentials in Zoho API Console
+3. Ensure all required modules are enabled in Zoho CRM
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/STAGING-MODE.md b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/STAGING-MODE.md
new file mode 100644
index 00000000..4527c14d
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/STAGING-MODE.md
@@ -0,0 +1,95 @@
+# Zoho CRM Integration - Staging Mode
+
+## Overview
+
+The Zoho CRM integration has a built-in staging mode to prevent accidental data synchronization from development or staging environments to the production Zoho CRM database.
+
+## How It Works
+
+### Domain Detection
+- **Production Domain**: `upskillhvac.com`
+- **Staging Domains**: All other domains (e.g., `*.cloudwaysapps.com`)
+
+### Staging Mode Behavior
+
+When running on any domain other than `upskillhvac.com`:
+
+1. **Read Operations**: Allowed (GET requests)
+2. **Write Operations**: Blocked (POST, PUT, DELETE, PATCH requests)
+3. **Visual Indicators**: Admin interface shows "STAGING MODE ACTIVE" banner
+4. **Sync Simulation**: Shows what data would be synced without actually sending it
+
+### Production Mode
+
+When running on `upskillhvac.com`:
+- All operations are allowed
+- Data syncs directly to Zoho CRM
+- No staging mode indicators
+
+## Admin Interface
+
+### Staging Mode Indicators
+- Blue info banner at the top of the Zoho CRM Sync page
+- Shows current site URL
+- Displays "STAGING MODE - Simulation Results" on sync operations
+
+### Test Data Preview
+In staging mode, sync operations return:
+- Total records that would be synced
+- Detailed preview of first 5 records
+- Field mappings that would be used
+
+## Implementation Details
+
+### Class: `HVAC_Zoho_Sync`
+```php
+private function is_sync_allowed() {
+ $site_url = get_site_url();
+ return strpos($site_url, 'upskillhvac.com') !== false;
+}
+```
+
+### Class: `HVAC_Zoho_CRM_Auth`
+```php
+// Blocks write operations in staging mode
+if ($is_staging && in_array($method, array('POST', 'PUT', 'DELETE', 'PATCH'))) {
+ return [simulated response];
+}
+```
+
+## Testing in Staging
+
+1. Access WordPress Admin → HVAC Community Events → Zoho CRM Sync
+2. See "STAGING MODE ACTIVE" banner
+3. Click sync buttons to see simulated results
+4. Review test data in expandable preview sections
+5. No actual data is sent to Zoho CRM
+
+## Deploying to Production
+
+1. Deploy code to `upskillhvac.com`
+2. Staging mode automatically deactivates
+3. Sync operations will write to Zoho CRM
+4. Monitor first sync carefully
+
+## Configuration
+
+No configuration needed - staging mode is automatic based on domain detection.
+
+## Security Benefits
+
+- Prevents test data from polluting production CRM
+- Allows safe testing of sync logic
+- No configuration mistakes possible
+- Clear visual indicators prevent confusion
+
+## Troubleshooting
+
+### Staging Mode Not Activating
+- Check site URL with `get_site_url()`
+- Ensure domain doesn't contain "upskillhvac.com"
+
+### Production Sync Not Working
+- Verify site URL contains "upskillhvac.com"
+- Check OAuth credentials are configured
+- Review error logs for API issues
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/TESTING.md b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/TESTING.md
new file mode 100644
index 00000000..cb1526d2
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/TESTING.md
@@ -0,0 +1,97 @@
+# Testing Zoho CRM Integration
+
+## Prerequisites
+
+Your `.env` file now contains:
+- `ZOHO_CLIENT_ID` ✓
+- `ZOHO_CLIENT_SECRET` ✓
+
+## Testing Process
+
+### Option 1: Using the Test Script (Recommended)
+
+1. Open a terminal and run:
+ ```bash
+ cd /Users/ben/dev/upskill-event-manager/wordpress-dev
+ ./bin/test-zoho-integration.sh
+ ```
+
+2. When prompted, choose 'y' to start the OAuth callback server
+
+3. Open a new terminal and run the script again, choosing 'n' this time
+
+4. Follow the prompts:
+ - Open the authorization URL in your browser
+ - Log in to Zoho and authorize the app
+ - Copy the authorization code from the callback page
+ - Paste it in the terminal
+
+### Option 2: Manual Testing
+
+1. Generate the authorization URL:
+ ```
+ https://accounts.zoho.com/oauth/v2/auth?
+ scope=ZohoCRM.settings.all,ZohoCRM.modules.all,ZohoCRM.users.all,ZohoCRM.org.all&
+ client_id=1000.Z0HOF1VMMJ9W2QWSU57GVQYEAVUSKS&
+ response_type=code&
+ access_type=offline&
+ redirect_uri=http://localhost:8080/callback&
+ prompt=consent
+ ```
+
+2. Open the URL in your browser
+
+3. After authorization, copy the code from the redirect URL
+
+4. Run the test script:
+ ```bash
+ cd /Users/ben/dev/upskill-event-manager/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho
+ php test-integration.php
+ ```
+
+5. Paste the authorization code when prompted
+
+## What the Test Does
+
+1. **Validates Credentials** - Checks that your client ID and secret work
+2. **Gets Tokens** - Exchanges the auth code for access and refresh tokens
+3. **Fetches Org Info** - Gets your Zoho organization details
+4. **Tests Module Access** - Verifies access to Campaigns, Contacts, and Invoices
+5. **Creates Config File** - Saves all credentials to `zoho-config.php`
+6. **Updates .env** - Adds the refresh token for future use
+
+## Expected Output
+
+You should see:
+- ✓ Credentials loaded from .env file
+- ✓ Tokens received successfully
+- ✓ Organization found
+- ✓ Campaigns module accessible
+- ✓ Contacts module accessible
+- ✓ Invoices module accessible
+- ✓ Configuration file created
+
+## Next Steps
+
+After successful testing:
+1. The system is ready for field creation
+2. You can start syncing data
+3. Check WordPress admin → Events → Zoho CRM for status
+
+## Troubleshooting
+
+### "Invalid Client" Error
+- Verify the client ID and secret in your .env file
+- Check that you're using the correct Zoho data center (US, EU, IN, AU)
+
+### "Invalid Code" Error
+- Authorization codes expire in ~1 minute
+- Generate a new code and use it immediately
+
+### Connection Issues
+- Make sure you can access https://accounts.zoho.com
+- Check if you need to use a different regional URL
+
+### Module Access Issues
+- Ensure all required modules are enabled in your Zoho CRM
+- Check that your Zoho plan includes API access
\ No newline at end of file
diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/auth-server.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/auth-server.php
new file mode 100644
index 00000000..d94718f9
--- /dev/null
+++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/zoho/auth-server.php
@@ -0,0 +1,57 @@
+";
+ $response .= "Authorization Successful! ";
+ $response .= "Authorization code received. You can close this window.
";
+ $response .= "Code: $auth_code
";
+ $response .= "Copy this code and paste it in the terminal.
";
+ $response .= "